[C++] 382 SwitchBot管理アプリの製作 その9 UserDefaultsでデータ永続化 Objective-C++, wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

設定データを保存する方法としてCSVを考えていましたが、macOSアプリですからOS固有の方法を活用してみたくなりました。

Objective-C++で書いた関数を使って設定データをUserDefaultsで管理するようにしました。

以前開発していたBBSブラウザと同様、cppファイルとmmファイルがプロジェクトに混在する形となります。

#include <iostream>
#import <Foundation/Foundation.h>

void saveToUserDefaults(const std::string& key, const std::string& value) {
    NSString *nsKey = [NSString stringWithUTF8String:key.c_str()];
    NSString *nsValue = [NSString stringWithUTF8String:value.c_str()];
    [[NSUserDefaults standardUserDefaults] setObject:nsValue forKey:nsKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

std::string getFromUserDefaults(const std::string& key) {
    NSString *nsKey = [NSString stringWithUTF8String:key.c_str()];
    NSString *nsValue = [[NSUserDefaults standardUserDefaults] stringForKey:nsKey];
    if (nsValue) {
        return std::string([nsValue UTF8String]);
    } else {
        return "";
    }
}

[Obj-C++] 22 XLSX変換アプリ完成

[M1 Mac, Big Sur 11.6.7, clang 13.0.0, no Xcode]

ようやくXLSX変換アプリが完成しました。

次はC++で新アプリ製作を予定しています。Objective-C++で作りたいところですが、クロスプラットフォームにしたいのでC++が妥当かと。

リストからxlsxファイルとcsvファイルを作成

[Obj-C++] 21 接尾語の検索およびアラート表示 : hasSuffix, NSAlert

[M1 Mac, Big Sur 11.6.7, clang 13.0.0, no Xcode]

TextFieldにペーストしたファイルパスの接尾語として.xlsxが含まれていなければアラート表示するようにしました。いわゆるモーダルダイアログです。

これまで扱ってきたプログラミング言語ではhasSuffixのような便利なメソッドはなかったので少し感動しました。もちろんhasPrefixで接頭語も検索できます。

先日、筋悪なコード絡みでボツにしたNSWindowの座標取得が早くも日の目を見ました。アラートの絶対座標設定に使っています。

<case1のみ>

// NSWindowを取得
NSArray *windowsArray = [NSApp orderedWindows];
NSLog(@"windowsArray %@", windowsArray);
NSWindow *currentWindow = [windowsArray objectAtIndex:0];

// NSWindowの絶対座標を取得し、アラートの原点となる左下座標のNSPointを作成
NSRect contentRect = [currentWindow frame];
float x = NSMinX(contentRect);
float y = NSMinY(contentRect);
NSPoint xy = NSMakePoint(x+50, y-150);

switch (rbtn_num){
		case 1:{
			path = [_delegateW getTextBox1];
			BOOL judge = [path hasSuffix:@".xlsx"];

			if (!judge){
				NSAlert *alert = [[NSAlert alloc] init];
				[alert.window setFrameOrigin:xy];
				[alert.window makeKeyAndOrderFront:nil];
				[alert addButtonWithTitle:@"OK"];
				[alert addButtonWithTitle:@"キャンセル"];
				alert.showsSuppressionButton = false;
				alert.messageText = @"This input is invalid!";
				// alert.informativeText = @"詳細情報";
				alert.alertStyle = NSAlertStyleWarning;
				[alert runModal];

				// 今回はOK, キャンセルで閉じるだけ
				// 処理を変える場合は下記参照
				// NSModalResponse ret = [alert runModal];
				// switch (ret) {
				// 	case NSAlertFirstButtonReturn:
				// 		NSLog(@"OK");
				// 		NSLog(@"suppressionButton:%ld", alert.suppressionButton.state);
				// 		break;
				// 	case NSAlertSecondButtonReturn:
				// 		NSLog(@"キャンセル");
				// 		break;
				// 	default:
				// 		break;
				// }

				break;

			}

			convert = [[Convert alloc] init];
			result = [convert ConvertFunc:path number:rbtn_num];
			[textview setString:result];
			break;
		}

参考サイト

[Obj-C++] 20 AppDelegateの作成方法(非Xcode環境)

[M1 Mac, Big Sur 11.6.5, clang 13.0.0, no Xcode]

ファイルのDrag & Dropを実装するにあたり一番苦労したのがAppDelegateの作成です。

@protocolはJavaで言う”抽象メソッドを設定したインターフェイス”であり、@propertyはそのインターフェイスを実装した新しいクラスといったところでしょうか。

今回のケースでは@propertyのdelegateW(緑色の下線)が@protocolを実装したクラスであり、@interfaceのプロパティ(ドットでアクセス可能)でもあるということになります。

設定方法
1.delegate元のヘッダファイルに@protocolと@interface内@propertyを設定する。
2.AppDelegate.mmに@protocolで設定したメソッドの内容を記述する。
3.設定したメソッドをdelegate元のmmファイルで使用する。

[Obj-C++] 19 NSWindowの重複 : 正攻法の解決手段 / 仕上げ

[M1 Mac, Big Sur 11.6.5, clang 13.0.0, no Xcode]

AppDelegateにDrag & Dropに関連する操作を集約させて仕上げました。異常終了についても修正しました。

NSWindow初期化, NSTextField・NSView作成をAppDelegateに移管しています。

初学者にはかなりきつい内容でしたが、それなりに基礎固めできたように思います。Objective-C++を学び始めて2週間ですから進度としてはまずまずです。

#import "AppDelegate.h"
#import "DragAndDropView.h"
#import "ConvertorWindow.h"

@interface AppDelegate() <DragAndDropViewDelegate,
                            ConvertorWindowDelegate>
@property (nonatomic, assign) DragAndDropView* view_dad;
@property (nonatomic, assign) ConvertorWindow* windowInit;
@property (nonatomic, assign) NSTextField* textBox1;
@end

@implementation AppDelegate
- (void)applicationWillFinishLaunching:(NSNotification*)aNotification {

    // NSWindow
    _windowInit = [[ConvertorWindow alloc] init];
    [_windowInit autorelease];
    [_windowInit makeKeyAndOrderFront:NSApp];
	[_windowInit setTitle:@"Xlsx Convertor"];

    // NSTextField
    _textBox1 = [[[NSTextField alloc] initWithFrame:NSMakeRect(50, 265-25-10, 220, 25)] autorelease];
	[_textBox1 setStringValue:@""];
	[_textBox1 setEditable:YES];
	[[_textBox1 window] makeFirstResponder:nil];
	[[_textBox1 currentEditor] moveToEndOfLine:nil];
    [[_windowInit contentView] addSubview:_textBox1];

    // NSView
	_view_dad = [[DragAndDropView alloc] initWithFrame:NSMakeRect(0, 0, 360, 265)];
	[_view_dad setWantsLayer:NO];
	_view_dad.layer.backgroundColor = [[NSColor orangeColor] CGColor];
    [[_windowInit contentView] addSubview:_view_dad];

    NSLog(@"%s","applicationWillFinishLaunching");
}
    
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    _view_dad.delegateV = self;
    _windowInit.delegateW = self;
}

- (void)applicationWillTerminate:(NSNotification *)aNotification {
}

- (void)pasteFunction:(NSString *)fileURL;{
    NSLog(@"%s","pasteFunction kaishi");
    _textBox1.stringValue = fileURL;
    NSLog(@"%s","pasteFunction kanryou");
}

- (void)clearTextBox1;{
    _textBox1.stringValue = @"";
}

- (NSString*)getTextBox1;{
    NSString* path = [_textBox1 stringValue];
    NSLog(@"path %@",path);
    return path;
}

@end
#include <Cocoa/Cocoa.h>

@protocol ConvertorWindowDelegate <NSObject>
- (void)clearTextBox1;
- (NSString*)getTextBox1;
@end

@interface ConvertorWindow : NSWindow
@property(nonatomic, assign) id <ConvertorWindowDelegate> delegateW;
@end
<関連箇所のみ>

- (BOOL)windowShouldClose:(id)sender {
	[NSApp terminate:sender];
	return YES;
}

- (void) OnButton1Click:(id)sender {
	onoff_XlsxToList = radioButton_a1.state; 
	onoff_XlsxToCsv = radioButton_a2.state; 
	onoff_ListToXlsx = radioButton_b1.state; 
	onoff_ListToCsv = radioButton_b2.state; 
	onoff_CsvToXlsx = radioButton_c1.state; 
	onoff_CsvToList = radioButton_c2.state;

	if (radioButton_a1.state == 1){
		rbtn_num = 1;
	} else if (radioButton_a2.state == 1){
		rbtn_num = 2;
	} else if (radioButton_b1.state == 1){
		rbtn_num = 3;
	} else if (radioButton_b2.state == 1){
		rbtn_num = 4;
	} else if (radioButton_c1.state == 1){
		rbtn_num = 5;
	} else if (radioButton_c2.state == 1){
		rbtn_num = 6;
	}

	NSLog(@"case %d",rbtn_num);
	
	switch (rbtn_num){
		case 1:
		case 2:
		case 5:
		case 6:
			path = [_delegateW getTextBox1];
			convert = [[Convert alloc] init];
			result = [convert ConvertFunc:path number:rbtn_num];
			[textview setString:result];
			break;
		case 3:
		case 4:
			list = [textBox2 stringValue];
			convert = [[Convert alloc] init];
			result = [convert ConvertFunc:list number:rbtn_num];
			[textview setString:result];
			break;
	}
}

- (void) OnButton2Click:(id)sender {
	[_delegateW clearTextBox1];
	textBox2.stringValue = @"";
}

[Obj-C++] 18 NSWindowの重複 : 正攻法の解決手段

[M1 Mac, Big Sur 11.6.5, clang 13.0.0, no Xcode]

ここまで書いておきながら恐縮ですが、前回以前の解決方法は率直に言って邪道だと思います。そもそもDrag & Dropする度にNSWindowを初期化するというのはどう考えても不健全です。筋悪なコードを修正しても傷口が広がるだけですから根本的な対策が必要になります。

そこでNSWindowを再初期化せずにDrag & Dropしたファイルのパスをセットする方法を考えました。

ただこのコードのままではNSWindowを閉じた時に異常終了するので何らかの修正が必要です。

#import <Cocoa/Cocoa.h>

@interface AppDelegate : NSObject <NSApplicationDelegate>{
}
@end
#import "AppDelegate.h"
#import "DragAndDropView.h"

@interface AppDelegate () <DragAndDropViewDelegate>
@property (nonatomic, assign) DragAndDropView* view_dad;
@property (nonatomic, assign) NSWindow* windowInit;
@property (nonatomic, assign) NSTextField* textBox1;
@end

@implementation AppDelegate
- (void)applicationWillFinishLaunching:(NSNotification*)notification {
    int height = [[NSScreen mainScreen] frame].size.height;

    _windowInit = [[NSWindow alloc] initWithContentRect:NSMakeRect(100, height-265-100, 360, 265) 
        styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | 
        NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable backing:NSBackingStoreBuffered defer:NO];
    [_windowInit autorelease];
    [_windowInit makeKeyAndOrderFront:NSApp];
	[_windowInit setTitle:@"Xlsx Convertor"];

    // FILE
	NSTextField* label1 = [[[NSTextField alloc] initWithFrame:NSMakeRect(10, 265-16-15, 34, 16)] autorelease];
	[label1 setFont:[NSFont fontWithName:@"Arial" size:14]];
	[label1 setStringValue:@"FILE"];
	[label1 setBezeled:NO];
	[label1 setDrawsBackground:NO];
	[label1 setEditable:NO];
	[label1 setSelectable:NO];
    [[_windowInit contentView] addSubview:label1];

    _textBox1 = [[[NSTextField alloc] initWithFrame:NSMakeRect(50, 265-25-10, 220, 25)] autorelease];
	[_textBox1 setStringValue:@""];
	[_textBox1 setEditable:YES];
	[[_textBox1 window] makeFirstResponder:nil];
	[[_textBox1 currentEditor] moveToEndOfLine:nil];
    [[_windowInit contentView] addSubview:_textBox1];

    // NSView
	_view_dad = [[DragAndDropView alloc] initWithFrame:NSMakeRect(0, 0, 360, 265)];
	[_view_dad setWantsLayer:NO];
	_view_dad.layer.backgroundColor = [[NSColor orangeColor] CGColor];
    [[_windowInit contentView] addSubview:_view_dad];

    NSLog(@"%s","applicationWillFinishLaunching");
}
    
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    _view_dad.delegate = self;
}

- (void)applicationWillTerminate:(NSNotification *)aNotification {
}

- (void)testFunction:(NSString *)fileURL;{
    NSLog(@"%s","testFunction kaishi");
    _textBox1.stringValue = fileURL;
    NSLog(@"%s","testFunction kanryou");
}
@end
#import <Cocoa/Cocoa.h>

@protocol DragAndDropViewDelegate <NSObject>
- (void)testFunction:(NSString *)fileURL;
@end

@interface DragAndDropView : NSView
@property(nonatomic, assign) id <DragAndDropViewDelegate> delegate;
@end
#import "AppDelegate.h"
#import "DragAndDropView.h"

@interface DragAndDropView()
@property (nonatomic) BOOL highlight;
@end

@implementation DragAndDropView
// 初期化
- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setHighlight:NO];
        [self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeFileURL]];
    }
    return self;
}

// Viewの描画処理
- (void)drawRect:(NSRect)rect{
    [super drawRect:rect];
    if (_highlight) {
        [[NSColor systemBlueColor] set];
        [NSBezierPath setDefaultLineWidth: 3];
        [NSBezierPath strokeRect: [self bounds]];
    }
}

// viewの境界にファイルがドラッグされた時の処理
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender{
    [self setHighlight:YES];
    [self setNeedsDisplay: YES];
    NSLog(@"%s","draggingEntered");

    return NSDragOperationCopy;
}

// ドラッグ中の処理
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender{
    [self setHighlight:YES];
    [self setNeedsDisplay: YES];

    return NSDragOperationCopy;
}

// ドラッグ中止時の処理
- (void)draggingExited:(id <NSDraggingInfo>)sender{
    [self setHighlight:NO];
    [self setNeedsDisplay: YES];
    NSLog(@"%s","draggingExited");
}

// ドロップ時の処理
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender {
    [self setHighlight:NO];
    [self setNeedsDisplay: YES];
    NSLog(@"%s","prepareForDragOperation");

    return YES;
}

// ドロップ後の処理
- (BOOL)performDragOperation:(id < NSDraggingInfo >)sender {
    NSLog(@"%s","performDragOperation kaishi");
    NSArray *draggedFilenames = [[sender draggingPasteboard] propertyListForType:NSPasteboardTypeFileURL];
    NSLog(@"draggedFilenames %@",draggedFilenames);

    return YES;
}

// ドロップ完了後の処理
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender{
    NSPasteboard *pboard = [sender draggingPasteboard];
    NSString *fileURL = [[NSURL URLFromPasteboard:pboard] path];
    NSLog(@"fileURL %@",fileURL);

    [_delegate testFunction:fileURL];

    NSLog(@"%s","concludeDragOperation kanryou");
}
@end

参考サイト

[Obj-C++] 17 NSWindowの重複 : 解決手順改良

[M1 Mac, Big Sur 11.6.5, clang 13.0.0, no Xcode]

前回の続きです。

前の記事で紹介した方法では、新しいNSWindowの位置がおまかせなので環境によってはピョンと飛んだ感じになり見栄えが良くないです。

微動だにしないようにするには以下のコードになります。

参考にさせていただいたサイトのことは前から知っていましたが、今回かなり役に立ちました。

<該当箇所のみ>

@implementation DragAndDropView

// ドロップ完了後の処理
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender{
    NSPasteboard *pboard = [sender draggingPasteboard];
    NSString *fileURL = [[NSURL URLFromPasteboard:pboard] path];
    NSLog(@"fileURL %@",fileURL);

    // NSAppのWindowをディスプレイ表示順に並べた配列を作成
    NSArray *windowsArray = [NSApp orderedWindows];
    NSLog(@"windowsArray %@", windowsArray);

    // 先頭のWindowを選択(操作しているため、このAppが常に先頭)
    NSWindow *currentWindow = [windowsArray objectAtIndex:0];

    // Windowのスクリーン座標を取得し、原点となる左下座標のNSPointを作成
    NSRect contentRect = [currentWindow frame];
    float x = NSMinX(contentRect);
    float y = NSMinY(contentRect);
    NSPoint xy = NSMakePoint(x, y);

    // Windowを非表示にする
    [currentWindow setIsVisible:NO];

    //新たにAppのWindowを作成
    ConvertorWindow *newWindow = [[ConvertorWindow alloc] init];
    [[newWindow autorelease] makeMainWindow];

    // Windowを元の位置に配置
    [newWindow setFrameOrigin:xy];
}
@end

参考記事1
参考記事2

[Obj-C++] 16 NSWindowの重複 : 解決手順2

[M1 Mac, Big Sur 11.6.5, clang 13.0.0, no Xcode]

前回の続きです。

AppDelegateファイルを作成以降は以下の流れになります。

1.Drag & Dropを設定しているNSViewクラスのconcludeDragOperationメソッドにDropしたファイルのパスについて処理内容を記述する。
2.NSAppクラスのorderedWindowsメソッドでNSWindowの配列をディスプレイ表示順にて取得する。
3.先頭のNSWindowを選択し、setIsVisibleメソッドで非表示に設定する。
4.[alloc init]で新しいNSWindowを作成する。
5.NSWindow内のTextFieldにファイルパスを入力する。

念のため消費メモリを確認しましたが、特に異常はありませんでした。ただ何らかの弊害が発生する可能性はあります。

コードは以下の通りです。

<該当箇所のみ>

@implementation DragAndDropView

// ドロップ完了後の処理
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender{
    NSPasteboard *pboard = [sender draggingPasteboard];
    NSString *fileURL = [[NSURL URLFromPasteboard:pboard] path];
    NSLog(@"fileURL %@",fileURL);

    // NSAppのWindowをディスプレイ表示順に並べた配列を作成
    NSArray *windowsArray = [NSApp orderedWindows];
    NSLog(@"windowsArray %@", windowsArray);

    // 先頭のWindowを非表示にする(操作しているため、このAppが常に先頭)
    NSWindow *currentWindow = [windowsArray objectAtIndex:0];
    [currentWindow setIsVisible:NO];

    //新たにAppのWindowを作成
    ConvertorWindow *newWindow = [[ConvertorWindow alloc] init];
    [[newWindow autorelease] makeMainWindow];

    // Window内のtextBox1にファイルパスを入力する
    newWindow->textBox1.stringValue = fileURL;
}
@end

参考サイト

[Obj-C++] 15 NSWindowの重複 : 解決手順1

[M1 Mac, Big Sur 11.6.5, clang 13.0.0, noXcode]

まずコードのメンテナンス性を高めてNSWindowを扱いやすくするため、mainクラスからAppDelegateクラスへNSWindowの起動時初期化を委譲しました。

次回は具体的なソースコードの内容について書きます。

#import "AppDelegate.h"

int main(int argc, const char * argv[]) {
    auto app = [NSApplication sharedApplication];
    app.delegate = [AppDelegate new];
    [app run];
}
#import "AppDelegate.h"
#import "ConvertorWindow.h"
#import "DragAndDropView.h"

@interface AppDelegate()
@end

@implementation AppDelegate

- (void)applicationWillFinishLaunching:(NSNotification*)notification {
    [[[[ConvertorWindow alloc] init] autorelease] makeMainWindow];
    NSLog(@"%s","applicationWillFinishLaunching");
    
}

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    NSLog(@"%s","applicationDidFinishLaunching");
}


- (void)applicationWillTerminate:(NSNotification *)notification {
}


- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
    return YES;
}
@end

[Obj-C++] 14 NSWindowの重複 : 不具合発覚

[M1 Mac, Big Sur 11.6.5, clang 13.0.0, noXcode]

開発中のアプリについてmainクラスと他のクラスでNSWindowの初期化を設定しているため、一定条件で重複表示になることが発覚しました。

コードを書きながら矛盾を感じていたのですが、見た目には問題がなかったのでそのままにしていました。macOS Montereyでも動作を確認しようとアプリを起動してファイルをDrag & Dropすると新しいNSWindowがずれて表示されたために気付いたという次第です。なおファイルパスを貼り付ける場合はダブりにはなりません。

この部分の修正には1日半かかりました。解決してホッとすると同時に言語仕様に対する違和感が残りました。たまたまかもしれませんが、JavaやC++(FLTK)ではこんなところで詰まることはありませんでした。アクセス修飾子やstatic修飾子を駆使して対処してきました。

次回以降の記事で対策を紹介します。