[Obj-C] 11 GUIアプリ : switch文

[M1 Mac, Big Sur 11.6.5, no Xcode]

ONになっているラジオボタンによって処理を変えるためswitch文を書きました。if文と同様、C言語の書き方になります。

Objective-Cを学び始めてからずっと引っかかっているのが関数の引数です。第2引数以降に引数ラベルを付けるというのが何とも独特です。

さすがにSwiftでは廃止だろうと思うところですが、逆に第1引数にも引数ラベルを付けるようになっています。

まさかと思いつつさらに調べていたら、どうも名前つき引数導入はトレンドだということが分かりました。確かに引数を順番に並べなくてもよくて便利ではありますが、引数ラベルをわざわざ書くのが面倒なので小規模開発の私には不要です。

<該当箇所のみ>

- (IBAction) 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 {
		rbtn_num = 6;
	}

	NSLog(@"%d",rbtn_num);

	NSString *path = [textBox1 stringValue];

	Convert *convert = [[Convert alloc] init];
	NSString *result = [convert ConvertFunc:path number:rbtn_num];
	[textview setString:result];
}
#include <Cocoa/Cocoa.h>
#include "process.h"
#include </Library/Frameworks/Python.framework/Versions/3.10/include/python3.10/Python.h>

@implementation Convert

- (NSString *)ConvertFunc:(NSString *)path number:(int)num {
    switch (num) {
    case 1:{
        Py_Initialize();
        // pyファイルの指定
        PyObject* myModuleString = PyUnicode_FromString((char*)"test");

        // pyファイルのモジュール化
        PyObject* myModule = PyImport_Import(myModuleString);

        // pyファイル内の関数を指定
        PyObject* myFunction = PyObject_GetAttrString(myModule,(char*)"xlsx_to_list");

        // 関数の引数を設定
        PyObject* args = PyTuple_Pack(1,PyUnicode_FromString((char *) [path UTF8String]));

        // 関数を実行し戻り値をPyObjectとして取得
        PyObject* myResult = PyObject_CallObject(myFunction,args);

        // PyObjectをconst char*に変換
        const char* result = PyUnicode_AsUTF8(myResult);
        NSString* result_ns = [NSString stringWithCString: result encoding:NSUTF8StringEncoding];

        NSLog(@"%@",result_ns);

        return result_ns;
        Py_FinalizeEx();
        break;
    }
    case 2:
        return @"case 2";
        break;
    case 3:
        return @"case 3";
        break;
    case 4:
        return @"case 4";
        break;
    case 5:
        return @"case 5";
        break;
    case 6:
        return @"case 6";
        break;
    default:
        return @"default";
        break;
    }
}
@end

[Obj-C] 10 GUIアプリ : NSScrollView, NSTextView

[M1 Mac, Big Sur 11.6.5, no Xcode]

垂直スクロールバー付きNSTextViewを配置しました。

NSScrollViewを配置してからNSTextViewをはめ込むという手法です。

Apple公式ドキュメントの内容を丸々拝借しました。直接リンクでアクセス可能です。

<該当箇所のみ>

NSScrollView* scrollview;
NSTextView* theTextView;

// NSScrollView
scrollview = [[[NSScrollView alloc] initWithFrame:NSMakeRect(20, 265-100-155, 320, 100)] autorelease];
NSSize contentSize = [scrollview contentSize];
[scrollview setBorderType:NSNoBorder];
[scrollview setHasVerticalScroller:YES];
[scrollview setHasHorizontalScroller:NO];
[scrollview setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];

// NSTextView
theTextView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, contentSize.width, contentSize.height)];
[theTextView setMinSize:NSMakeSize(0.0, contentSize.height)];
[theTextView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
[theTextView setVerticallyResizable:YES];
[theTextView setHorizontallyResizable:NO];
[theTextView setAutoresizingMask:NSViewWidthSizable];
[[theTextView textContainer] setContainerSize:NSMakeSize(contentSize.width, FLT_MAX)];
[[theTextView textContainer] setWidthTracksTextView:YES];

[scrollview setDocumentView:theTextView];

Putting an NSTextView Object in an NSScrollView

[Obj-C] 09 GUIアプリ : Python/C API用Makefile

[M1 Mac, Big Sur 11.6.5, no Xcode]

Python/C APIに対応したCMakeLists.txtの書き方が分からないので、Makefileに戻ってきました。小回りの効くところがMakefileの強みですね。

プロジェクトのファイルが増えてFinderが見にくくなったため、include、obj、src、binディレクトリを作成して配置し直しています。

Objective-Cでアプリを作れるようになったのは嬉しいですが、それよりもXcodeを起動せずにここまでやれたことの方が喜びが大きいです。

# コンパイラ
COMPILER = clang
DEBUG = -g

# オプション
CFLAGS = $(shell python3-config --includes)
LDFLAGS = $(shell python3-config --ldflags) -framework Cocoa

# includeパス(-I)
INCLUDE = -I./include -I/Library/Frameworks/Python.framework/Versions/3.10/include/python3.10

# ライブラリパス(-l)
LIBRARY0 = -lpython3.10

# 優先ライブラリパス(-L)
LIBRARY = -L/Library/Frameworks/Python.framework/Versions/3.10/lib

# ソースファイル
SRCDIR = ./src
SRCS = $(SRCDIR)/XlsxConvertor.m $(SRCDIR)/process.m

# オブジェクトファイル
OBJDIR = ./obj
OBJS = $(OBJDIR)/XlsxConvertor.o $(OBJDIR)/process.o

# 実行ファイル
TARGETDIR = ./bin
TARGET = XlsxConvertor

# コンパイル
$(OBJDIR)/XlsxConvertor.o: $(SRCDIR)/XlsxConvertor.m
	$(COMPILER) $(CFLAGS) $(INCLUDE) $(DEBUG) -o $@ -c $<

$(OBJDIR)/process.o: $(SRCDIR)/process.m
	$(COMPILER) $(CFLAGS) $(INCLUDE) $(DEBUG) -o $@ -c $<

# ビルド
$(TARGET):$(OBJS)
	$(COMPILER) -o $(TARGETDIR)/$@ $(OBJS) $(LDFLAGS) $(LIBRARY0) $(LIBRARY)

# ファイル削除&コンパイル
.PHONY : all
all: clean $(TARGET)

# ファイル削除
.PHONY : clean
clean:
	rm -rf $(OBJS) $(TARGETDIR)/$(TARGET)

# CMake用
.PHONY : test
test:
	rm -rf build/*
	cd /XlsxConvertor/build && cmake .. && make

[Obj-C] 08 GUIアプリ : Python/C APIの使用

[M1 Mac, Big Sur 11.6.5, no Xcode]

C++(FLTK)からObjective-Cへの移植がほぼ完了しました。実行ファイル作成までたどり着きました。

Excelファイルを読み込み、A列のセル値をリスト化して出力する機能を実装しています。使用しているライブラリはopenpyxlです。

あてにしていたPyObjCはどうやらPythonからCocoaなどのフレームワークやライブラリを利用するという目的で作られたものであり、Python/C APIをアレンジしたものではないことが分かってきました。冷静に考えればApple系開発者がそこまでするメリットはあまりないですね。

せっかくなのでPython/C APIで実装しましたが、appファイルを作成して実行してもC++と同様に落ちてしまうでしょう。試しにC++で作成したappファイル内にある実行ファイルを入れ替えるとNGでした。

Objective-Cは細かいところになると機能の少なさを露呈するものの外観のまとまりはいいので、コンソール付きのCocoaアプリとして残りの機能も実装しようと思います。

@interface Xlsx : NSObject

- (NSString *)XlsxToList:(NSString *)path;

@end
#include <Cocoa/Cocoa.h>
#include "process.h"
#include </Library/Frameworks/Python.framework/Versions/3.10/include/python3.10/Python.h>

@implementation Xlsx

- (NSString *)XlsxToList:(NSString *)path{

    Py_Initialize();

    // pyファイルの指定
    PyObject* myModuleString = PyUnicode_FromString((char*)"test");

    // pyファイルのモジュール化
    PyObject* myModule = PyImport_Import(myModuleString);

    // pyファイル内の関数を指定
    PyObject* myFunction = PyObject_GetAttrString(myModule,(char*)"xlsx_to_list");

    // 関数の引数を設定
    PyObject* args = PyTuple_Pack(1,PyUnicode_FromString((char *) [path UTF8String]));

    // 関数を実行し戻り値をPyObjectとして取得
    PyObject* myResult = PyObject_CallObject(myFunction,args);

    // PyObjectをconst char*へ変換
    const char* result = PyUnicode_AsUTF8(myResult);

    // NSStringへ変換
    NSString* result_ns = [NSString stringWithCString: result encoding:NSUTF8StringEncoding];

    return result_ns;

    Py_FinalizeEx();    

}
@end
#include <Cocoa/Cocoa.h>
#include "process.h"

<該当箇所のみ>

- (IBAction) OnButton1Click:(id)sender {
	NSString* path = [textBox1 stringValue];

	Xlsx* test = [[Xlsx alloc] init];
	NSString* result = [test XlsxToList:path];
	[textview setString:result];
}

[Obj-C] 07 GUIアプリ : NSTextView

[M1 Mac, Big Sur 11.6.5, no Xcode]

ラジオボタンとテキストエリアを配置しました。これでGUI自体は完成です。

ラジオボタンを簡単にグループ化できない、NSTextViewの枠線を容易に描けない、など機能面では比較的簡素とされているFLTKにも及ばないという印象です。

日進月歩のC++に比較して古き良きC言語テイストを保っているという感じでしょうか。

<該当箇所のみ>

// radioButton
radioButton_a1 = [[[NSButton alloc] initWithFrame:NSMakeRect(100, 265-20-75, 60, 20)] autorelease];
[radioButton_a1 setFont:[NSFont fontWithName:@"Arial" size:12]];
[radioButton_a1 setTitle:@"LIST"];
[radioButton_a1 setButtonType:NSButtonTypeRadio];
[radioButton_a1 setTarget:self];
[radioButton_a1 setAction:@selector(OnRadioClick:)];
[radioButton_a1 setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
[radioButton_a1 setState:NSControlStateValueOn];

// TextView
textview = [[[NSTextView alloc] initWithFrame:NSMakeRect(20, 265-100-155, 320, 100)] autorelease];
[textview setFont:[NSFont fontWithName:@"Arial" size:12]];
[textview setDrawsBackground:YES];
[textview setEditable:NO];
[textview setSelectable:NO];

[Obj-C] 05 GUIアプリ : NSButton

[M1 Mac, Big Sur 11.6.5, no Xcode]

ボタンを配置しました。ボタンタイトルの色を設定できないので、Windowの背景色をやむなくデフォルトに戻しました。

設定の仕方が単純ではないというだけでできないわけではないと思います。一部機能ではFLTKに及ばないと考えるのが妥当でしょう。

<該当箇所のみ>

button1 = [[[NSButton alloc] initWithFrame:NSMakeRect(290, 265-30-10, 60, 30)] autorelease];
[button1 setFont:[NSFont fontWithName:@"Arial" size:12]];
[button1 setTitle:@"実行"];
[button1 setBezelStyle:NSBezelStyleRegularSquare];
[button1 setTarget:self];
[button1 setAction:@selector(OnButton1Click:)];
[button1 setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];

[Obj-C] 04 GUIアプリ : NSTextField

[M1 Mac, Big Sur 11.6.5, no Xcode]

テキストラベルを配置しました。書き方自体はSwiftやFLTKと似ているため、角括弧への違和感が取れないながらも少しずつ慣れてきました。

Windowや図形のY座標は、ディスプレイやWindowの縦サイズから図形の高さと左上基準のY座標を引く必要がありややこしいです。

あとはテキストエリアとボタン類なのでGUIについてはスムーズに実装できそうです。

#include <Cocoa/Cocoa.h>

@interface ConvertorWindow : NSWindow {
  NSTextField* label1;
  NSTextField* label2;
}
- (instancetype)init;
- (BOOL)windowShouldClose:(id)sender;
@end

@implementation ConvertorWindow
- (instancetype)init {
	NSColor *background = [NSColor colorWithCalibratedRed:(double)247/255 green:(double)252/255 blue:(double)254/255 alpha:1.0f];
	NSColor *foreground = [NSColor colorWithCalibratedRed:(double)57/255 green:(double)63/255 blue:(double)76/255 alpha:1.0f];

	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];
	[label1 setTextColor:foreground];

	label2 = [[[NSTextField alloc] initWithFrame:NSMakeRect(10, 265-16-45, 34, 16)] autorelease];
	[label2 setFont:[NSFont fontWithName:@"Arial" size:14]];
	[label2 setStringValue:@"LIST"];
	[label2 setBezeled:NO];
	[label2 setDrawsBackground:NO];
	[label2 setEditable:NO];
	[label2 setSelectable:NO];
	[label2 setTextColor:foreground];

	[super initWithContentRect:NSMakeRect(100, 1080-265-100, 360, 265) styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable backing:NSBackingStoreBuffered defer:NO];
	[self setTitle:@"Xlsx Convertor"];
	[[self contentView] addSubview:label1];
	[[self contentView] addSubview:label2];
	[self setIsVisible:YES];
	[self setBackgroundColor:background];
	return self;
}

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

int main(int argc, char* argv[]) {
  [NSApplication sharedApplication];
  [[[[ConvertorWindow alloc] init] autorelease] makeMainWindow];
  [NSApp run];
}

[Obj-C] 03 GUIアプリ : アイコン設定

[M1 Mac, Big Sur 11.6.5, no Xcode]

CMakeが作成したMakefileにアイコン設定を追記しました。

以下のようにallの箇所に追記して、make allコマンドを実行するとappファイル作成&アイコン設定できます。このコマンドにより、imagesディレクトリにあるicnsファイルをappファイル内にコピーし、info.plistのkeyとvalueを上書きします。

# The main all target
all: cmake_check_build_system
	$(CMAKE_COMMAND) -E cmake_progress_start /code/ObjectiveC/projects/XlsxConvertor02/build/CMakeFiles /code/ObjectiveC/projects/XlsxConvertor02/build//CMakeFiles/progress.marks
	$(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 all
	$(CMAKE_COMMAND) -E cmake_progress_start /code/ObjectiveC/projects/XlsxConvertor02/build/CMakeFiles 0
	# 以下3行を追記
	mkdir XlsxConvertor.app/Contents/Resources
	cp ../images/XlsxConvertor.icns XlsxConvertor.app/Contents/Resources
	plutil -replace 'CFBundleIconFile' -string "XlsxConvertor.icns" XlsxConvertor.app/Contents/Info.plist
.PHONY : all

[Obj-C] 02 GUIアプリ : NSWindow

[M1 Mac, Big Sur 11.6.5, no Xcode]

まずはWindowを作成し、タイトルと背景色を設定しました。

何でもない内容ですが、手本となるサンプルコードを探すのに少々苦労しました。Xcodeを使わずに、エディタとターミナルだけでアプリを作成していきます。

Objective-Cの本を2冊購入しましたが、レガシー言語だからか状態の良い中古本で1冊800円程度でした。今のところパラパラと見た位で結局GitHubにある本とは関係のないサンプルコードを取っ掛かりの教材にしています。実践が先で理屈は後回しです。

Objective-CではMakefileがCMakeで簡単に作成できます。C++ではコンパイルやビルドのオプション探索に苦労していましたが、簡潔なCMakeLists.txtに丸投げで済みそうです。C++のCMakeはかなりとっつきにくい印象でした。

特徴的な点を以下に挙げておきます。

1.座標の原点が左上ではなく左下。左上基準の場合、ディスプレイの解像度がわからないとY座標が算出できない。
2.RGBはSwiftとは異なり、分数だけではNG。エラーにはならないが色がおかしくなる。(double)などで小数への変換が必要。
3.MakefileはCMakeに作らせても簡単。

#include <Cocoa/Cocoa.h>

int main(int argc, char* argv[]) {
  NSColor* background = [NSColor colorWithCalibratedRed:(double)247/255 green:(double)252/255 blue:(double)254/255 alpha:1.0f];

  NSWindow* window = [[[NSWindow alloc] initWithContentRect:NSMakeRect(100, 1080-220-100, 360, 220) styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable backing:NSBackingStoreBuffered defer:NO] autorelease];
  [window setIsVisible:YES];
  [window setTitle:@"Xlsx Convertor"];
  [window setBackgroundColor:background];
  [NSApplication sharedApplication];
  [NSApp run];
}
cmake_minimum_required(VERSION 3.1)

# Project
Project(XlsxConvertor VERSION 1.0.0)
find_library(COCOA_LIBRARY Cocoa)

# Options
set(MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION})
set(MACOSX_BUNDLE_COPYRIGHT "Copyright (C) 2022")
set(MACOSX_BUNDLE_INFO_STRING "The name of this app is XlsxConvertor")
set(MACOSX_BUNDLE_GUI_IDENTIFIER "xxx.xxx.XlsxConvertor")

# XlsxConvertor
add_executable(${PROJECT_NAME} MACOSX_BUNDLE XlsxConvertor.m)
target_link_libraries(${PROJECT_NAME} ${COCOA_LIBRARY})

参考サイト