[C++] 376 SwitchBot管理アプリの製作 その3 ボタンのカスタマイズ wxWidgets

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

ボタンの角を丸くして枠を消しラベル色と背景色を指定できるRoundedButtonクラスをChatGPTに作成してもらいました。当然ヘッダファイルもさくっと作ってくれます。

FLTKを苦労して学んでいたころからは信じられない楽さです。もっともあのころにC++の基本やMakefileの作り方を習得していたからできることではあります。

#include "RoundedButton.h"

RoundedButton::RoundedButton(wxWindow* parent, wxWindowID id, const wxString& label, const wxPoint& pos, const wxSize& size, const wxColour& bgColor, const wxColour& labelColor)
    : wxButton(parent, id, label, pos, size), m_bgColor(bgColor), m_labelColor(labelColor)
{
    SetBackgroundStyle(wxBG_STYLE_PAINT);
    Bind(wxEVT_PAINT, &RoundedButton::OnPaint, this);
}

void RoundedButton::OnPaint(wxPaintEvent& event)
{
    wxAutoBufferedPaintDC dc(this);
    wxSize size = GetSize();
    wxRect rect(0, 0, size.x, size.y);

    dc.SetBrush(wxBrush(m_bgColor));
    dc.SetPen(*wxTRANSPARENT_PEN);
    dc.DrawRoundedRectangle(rect, 10); // 角の半径を10に設定

    dc.SetTextForeground(m_labelColor); // ラベルの色を設定
    dc.DrawLabel(GetLabel(), rect, wxALIGN_CENTER);
}
// AUTOボタン
new RoundedButton(bottomPanel, wxID_ANY, "AUTO", wxPoint(100, 5), wxSize(60, 30), wxColour("#00FFFF"), wxColour("#C0C0C0")); // シアン背景、白ラベル

// OFFボタン
new RoundedButton(bottomPanel, wxID_ANY, "OFF", wxPoint(180, 5), wxSize(60, 30), wxColour("#FF00FF"), wxColour("#FFFFFF")); // マゼンタ背景、白ラベル

[C++] 375 SwitchBot管理アプリの製作 その2 GUIの作成 wxWidgets

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

wxWidgetsのコードはFLTKとはまた違った独特のクセがあります。

ChatGPTにGUIのコードを書いてもらいました。Swiftと遜色ない洗練された外観です。

やはりGUIは座標を使うのが楽です。SwiftUIやJavaのSwingは書きにくくて苦手です。

#include <wx/wx.h>
#include <wx/slider.h>
#include <wx/stattext.h>
#include <wx/choice.h>
#include <wx/panel.h>
#include <wx/sizer.h>
#include <wx/image.h>
#include <wx/bitmap.h>
#include "functions.h"

class MyFrame : public wxFrame
{
public:
    MyFrame() : wxFrame(NULL, wxID_ANY, "SwitchBot Manager", wxDefaultPosition, wxSize(600, 400))
    {
        // パネルを左右に分割
        wxPanel* leftPanel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(300, 400));
        wxPanel* rightPanel = new wxPanel(this, wxID_ANY, wxPoint(300, 0), wxSize(300, 400));

        // 背景色の設定
        leftPanel->SetBackgroundColour(wxColour("#00CED1"));
        rightPanel->SetBackgroundColour(wxColour("#4B0082"));

        // 下部のパネルを追加して背景色を設定
        wxPanel* bottomPanel = new wxPanel(leftPanel, wxID_ANY, wxPoint(0, 100), wxSize(300, 300));
        bottomPanel->SetBackgroundColour(wxColour("#008B8B"));

        // CSVからのデータ読み込み
        std::vector<double> data = ReadCsvData();

        // 室温と湿度
        new wxStaticText(leftPanel, wxID_ANY, "室温", wxPoint(10, 10), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.1f", data[0]), wxPoint(100, 10), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, "湿度", wxPoint(10, 40), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.0f", data[1]), wxPoint(100, 40), wxSize(80, 20));

        // 不快指数
        new wxStaticText(leftPanel, wxID_ANY, "不快指数", wxPoint(10, 70), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.0f", data[2]), wxPoint(100, 70), wxSize(80, 20));

        // エアコン
        new wxStaticText(bottomPanel, wxID_ANY, "エアコン", wxPoint(10, 10), wxSize(80, 20));

        // AUTOボタン
        new wxButton(bottomPanel, wxID_ANY, "AUTO", wxPoint(100, 5), wxSize(60, 30));

        // OFFボタン
        new wxButton(bottomPanel, wxID_ANY, "OFF", wxPoint(180, 5), wxSize(60, 30));

        // 動作温度
        new wxStaticText(bottomPanel, wxID_ANY, "動作温度", wxPoint(10, 51), wxSize(72, 18));
        wxSlider* operationTempSlider = new wxSlider(bottomPanel, wxID_ANY, 26, 26, 28, wxPoint(110, 50), wxSize(120, 20), wxSL_HORIZONTAL);
        wxStaticText* operationTempDisplay = new wxStaticText(bottomPanel, wxID_ANY, "26", wxPoint(240, 50), wxSize(40, 20));

        // 温度幅
        new wxStaticText(bottomPanel, wxID_ANY, "温度幅", wxPoint(10, 81), wxSize(54, 18));
        wxSlider* tempRangeSlider = new wxSlider(bottomPanel, wxID_ANY, 3, 1, 5, wxPoint(110, 80), wxSize(120, 20), wxSL_HORIZONTAL);
        wxStaticText* tempRangeDisplay = new wxStaticText(bottomPanel, wxID_ANY, "0.3", wxPoint(240, 81), wxSize(40, 20));

        // 設定温度
        new wxStaticText(bottomPanel, wxID_ANY, "設定温度", wxPoint(10, 111), wxSize(72, 18));
        wxSlider* setTempSlider = new wxSlider(bottomPanel, wxID_ANY, 22, 20, 25, wxPoint(110, 110), wxSize(120, 20), wxSL_HORIZONTAL);
        wxStaticText* setTempDisplay = new wxStaticText(bottomPanel, wxID_ANY, "22", wxPoint(240, 110), wxSize(40, 20));

        // ファン
        new wxStaticText(bottomPanel, wxID_ANY, "ファン", wxPoint(10, 141), wxSize(53, 18));
        wxArrayString fanChoices;
        fanChoices.Add("Auto");
        fanChoices.Add("Low");
        fanChoices.Add("Medium");
        fanChoices.Add("High");
        new wxChoice(bottomPanel, wxID_ANY, wxPoint(110, 140), wxSize(80, 20), fanChoices);

        // Event bindings
        operationTempSlider->Bind(wxEVT_SLIDER, [operationTempDisplay](wxCommandEvent& event) {
            operationTempDisplay->SetLabel(wxString::Format("%.1f", event.GetInt() / 10.0));
        });

        tempRangeSlider->Bind(wxEVT_SLIDER, [tempRangeDisplay](wxCommandEvent& event) {
            tempRangeDisplay->SetLabel(wxString::Format("%.1f", event.GetInt() / 10.0));
        });

        setTempSlider->Bind(wxEVT_SLIDER, [setTempDisplay](wxCommandEvent& event) {
            setTempDisplay->SetLabel(wxString::Format("%d", event.GetInt()));
        });
    }
};

class MyApp : public wxApp
{
public:
    virtual bool OnInit()
    {
        MyFrame* frame = new MyFrame();
        frame->Show(true);
        return true;
    }
};

wxIMPLEMENT_APP(MyApp);

[C++] 374 SwitchBot管理アプリの製作 その1 wxWidgets

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

wxWidgetsのHomebrewでの最新バージョンが3.2.3と遅れているのでGitHub最新の3.2.5を自分でビルドしました。zipではビルドできません。tar.bz2をダウンロードします。

# --with-libtiff=builtinを追加しないとエラーになる

mkdir build-cocoa-debug
cd build-cocoa-debug
../configure --enable-debug --with-libtiff=builtin
make

ビルドしたライブラリとインクルードは/usr/localにコピーしました。アプリのビルド時にwx/setup.hがないというエラーになりましたが、以下のパスにあったのでインクルードフォルダにコピーしました。

/wxWidgets-3.2.5/build-cocoa-debug/lib/wx/include/osx_cocoa-unicode-3.2/wx/setup.h

wxWidgetsを広く普及するためにも、せめてHomebrewに最新版を登録して欲しいところですが、人が足りないのかな。

Makefileは以下の通りです。

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

# フラグ
CPPFLAGS = -D_FILE_OFFSET_BITS=64 -DWXUSINGDLL -D__WXMAC__ -D__WXOSX__ -D__WXOSX_COCOA__  -std=c++17 
LDFLAGS = -framework IOKit -framework Carbon -framework Cocoa -framework QuartzCore -framework AudioToolbox -framework System -framework OpenGL -lwx_osx_cocoau_xrc-3.2 -lwx_osx_cocoau_html-3.2 -lwx_osx_cocoau_qa-3.2 -lwx_osx_cocoau_core-3.2 -lwx_baseu_xml-3.2 -lwx_baseu_net-3.2 -lwx_baseu-3.2 -lc++

# includeパス(-I)
INCLUDE = -I./include -I/Volumes/DATA_m1/code/cpp/mylib/include -I/usr/local/include/wxWidgets

# ライブラリ(-l)
LIBRARY0 =

# ライブラリパス(-L)
LIBRARY = -L/usr/local/lib/wxWidgets

# ソースファイル
SRCDIR = ./src
SRCS = $(shell find $(SRCDIR) -type f)

# オブジェクトファイル
OBJDIR = ./obj
OBJS = $(addprefix $(OBJDIR), $(patsubst ./src/%.cpp,/%.o,$(SRCS)))

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

# cppファイルからoファイル作成 $<:依存ファイル
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
	$(COMPILER) $(CPPFLAGS) $(INCLUDE) $(DEBUG) -o $@ -c $<

# oファイルから実行ファイルとappファイル作成
$(TARGET): $(OBJS)
	$(COMPILER) -o $(TARGETDIR)/$@ $(OBJS) $(LIBRARY0) $(LDFLAGS) $(LIBRARY)
	mkdir -p $(TARGET).app/Contents/MacOS
	mkdir -p $(TARGET).app/Contents/Resources
	cp $(TARGETDIR)/$(TARGET) $(TARGET).app/Contents/MacOS/$(TARGET)
	cp ./images/$(TARGET).icns $(TARGET).app/Contents/Resources
	echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" > $(TARGET).app/Contents/Info.plist
	echo "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" >> $(TARGET).app/Contents/Info.plist
	echo "<plist version=\"1.0\">" >> $(TARGET).app/Contents/Info.plist
	echo "<dict>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleExecutable</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleIconFile</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>$(TARGET).icns</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleIdentifier</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>com.yourcompany.$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleName</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleVersion</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>1.0</string>" >> $(TARGET).app/Contents/Info.plist
	echo "</dict>" >> $(TARGET).app/Contents/Info.plist
	echo "</plist>" >> $(TARGET).app/Contents/Info.plist

# 全ソース強制コンパイル
.PHONY:all
all: clean $(TARGET)

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

[SwitchBot] 08 管理アプリMac版の製作 その4 wxWidgetsに変更 C++

[Mac M2 Pro 12CPU, Sonoma 14.5]

SwiftUIではNSOpenPanelやfileImporterでファイル選択しないとローカルファイルを読み込めないことが判明したため、C++に戻りwxWidgetsに挑戦することにしました。

以前wxWidgetsを扱った時は実行ファイルは作成できてもappファイル作成ができなかったのですが、今回はChatGPTに手伝ってもらいあっさりMakefileを完成させました。

ウィジェットのデザインはOS依存ですから、Swiftアプリ風の見た目になります。

次の記事

[SwitchBot] 05 管理アプリMac版の製作 その1 C++, FLTK

[Mac M2 Pro 12CPU, Sonoma 14.5]

SwitchBot管理アプリMac版の製作に着手しました。手順は以下の通りです。

1.Adobe XDでGUIをデザイン。座標とサイズを自作プラグインで抽出
2.ChatGPT用プロンプトを作成しレスポンスを得る
3.手直ししたC++コードをビルド

このコードを土台に肉付けしていきます。

FLTKアプリを作成します
C++コードを考えてください
ウィンドウのサイズは600*400
各ウィジェットの座標は以下の通り
{
  "エアコングラフ": [15, 200, 270, 180],
  "ファン選択": [110, 140, 60, 20],
  "ファン": [10, 141, 53, 18],
  "温度幅表示": [240, 81, 40, 20],
  "温度幅スライダー": [110, 80, 120, 20],
  "温度幅": [10, 81, 54, 18],
  "動作温度表示": [240, 50, 40, 20],
  "動作温度スライダー": [110, 50, 120, 20],
  "動作温度": [10, 51, 72, 18],
  "OFFボタン": [190, 10, 60, 30],
  "AUTOボタン": [110, 10, 60, 30],
  "エアコン": [10, 15, 80, 20],
  "設定温度表示": [240, 110, 40, 20],
  "設定温度スライダー": [110, 110, 120, 20],
  "設定温度": [10, 111, 72, 18]
}

エアコン:Fl_Boxに"エアコン"を表示
AUTOボタン:ラベルが"AUTO"のFl_Button
OFFボタン:ラベルが"OFF"のFl_Button

動作温度:Fl_Boxに"動作温度"を表示
動作温度スライダー:Fl_Sliderを表示 26から28まで0.1きざみ
動作温度表示:Fl_Boxに動作温度スライダーの値を表示。

温度幅:Fl_Boxに"温度幅"を表示
温度幅スライダー:Fl_Sliderを表示 0.1から0.5まで0.1きざみ
温度幅表示:Fl_Boxに温度幅スライダーの値を表示。

設定温度:Fl_Boxに"設定温度"を表示
設定温度スライダー:Fl_Sliderを表示 20から25まで1.0きざみ
設定温度表示:Fl_Boxに設定温度スライダーの値を表示。

ファン:Fl_Boxに"ファン"を表示
ファン選択:ブルダウンで選択 # fan speed includes 1 (auto), 2 (low), 3 (medium), 4 (high);
エアコングラフ:pngファイルを表示

[C++] 373 ChatAIアプリの製作 その51 LLMの序列変更 Perplexity API

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

Perplexity APIを使うことで最新情報も取得できるようになったので、第1選択LLMに昇格させました。

間違った情報が混ざっていることもありますが、ラフな情報収集としてはまずまずかと思います。

24/05/17追記
Perplexity APIは回答の言語が不安定(日本語 or 英語)なので、第1選択LLMをgtp-4oに戻しました。Webサービス(無料 or Pro)で使う方が良さそうです。

[C++] 372 ChatAIアプリの製作 その50 gpt-4oとPerplexity APIの導入

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

5月13日に発表されたgpt-4oを早速導入しました。Perplexity APIも導入しました。

gpt-4oについてはこれから検証していきます。

まず驚いたのがPerplexity API(llama-3-sonar-large-32k-online)のネット検索能力です。年度、日付などはいい加減ですが、プレミアリーグの最新データを取得してくれました。順位、勝敗、得失点は正確でした。

ただしPerplexity APIはgptのようにあまり行間を読んでくれないため、くどくどしくプロンプトを書かないと意図に沿った回答が得られないケースがあります。

またsystemで日本語で回答するように指示しても、英語で返す方が多かったりします。なので最初のプロンプトでも日本語指定のテンプレを自動追記するようにしました。それでも頑なに英語で返すことがあります。

アプリに登録しているLLM一覧(左上のプルダウンメニュー)

[C++] 371 ローマ字を英数モードで打った時のリカバリー その3 再変換までキー5回押下で実装 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

かつてのATOKの様に、かなキー2回押下で再変換までできず、以下のように機能を3分割して実装しました。

英字からひらがな変換:かなキー2回押下、AppleScript
ひらがなを範囲選択:英数キー2回押下、 C++自製関数
再変換:F13キー1回押下、AppleScript

最終的にC++だけで実装できるよう、リファクタリングしていきます。

#include <ApplicationServices/ApplicationServices.h>

void pressShiftLeftArrow(int count) {
    // Shiftキーを押す
    CGEventRef shiftDown = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)56, true);
    CGEventPost(kCGHIDEventTap, shiftDown);
    CFRelease(shiftDown);

    // 文字数分だけ左矢印キーを押す
    for (int i = 0; i < count; ++i) {
        CGEventRef leftArrowDown = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)123, true);
        CGEventRef leftArrowUp = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)123, false);
        CGEventSetFlags(leftArrowDown, kCGEventFlagMaskShift);
        CGEventPost(kCGHIDEventTap, leftArrowDown);
        CGEventPost(kCGHIDEventTap, leftArrowUp);
        CFRelease(leftArrowDown);
        CFRelease(leftArrowUp);
        // イベント間にわずかな遅延を挿入
        usleep(10000); // 10ミリ秒待機
    }

    // Shiftキーを離す
    CGEventRef shiftUp = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)56, false);
    CGEventPost(kCGHIDEventTap, shiftUp);
    CFRelease(shiftUp);
}

[C++] 370 ローマ字を英数モードで打った時のリカバリー その2 かなキー連続押下対応

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

前回のコードでは、かなキーを何となく押し、英字を入力して無意識に2回目を押したりすると不要なひらがなが一気に出現します。

これではまずいので1秒以内に2回目を押した時だけひらがな変換するようにしました。

あとはひらがな変換した文字列を範囲選択して再変換するという処理ですが、流石にC++でもハードルは高そうです。

Carbonフレームワークが今も現役なことに少々驚きました。

#include <Carbon/Carbon.h>
#include <iostream>
#include <string>
#include <unordered_map>
#include <chrono>

std::unordered_map<CGKeyCode, std::string> keyCodeMap = {
    {0, "a"}, {11, "b"}, {8, "c"}, {2, "d"}, {14, "e"}, {3, "f"},
    {5, "g"}, {4, "h"}, {34, "i"}, {38, "j"}, {40, "k"}, {37, "l"},
    {46, "m"}, {45, "n"}, {31, "o"}, {35, "p"}, {12, "q"}, {15, "r"},
    {1, "s"}, {17, "t"}, {32, "u"}, {9, "v"}, {13, "w"}, {7, "x"},
    {16, "y"}, {6, "z"}
};

std::string inputBuffer;
int kanaKeyPressCount = 0;
std::chrono::steady_clock::time_point firstKanaKeyPressTime;

CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
    if (type == kCGEventKeyDown) {
        CGKeyCode keyCode = (CGKeyCode)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);

        if (keyCode == 104) { // かなキー(104)が押された場合
            std::cout << "かなキーを押しました" << std::endl;
            auto now = std::chrono::steady_clock::now();

            if (kanaKeyPressCount == 0) {
                firstKanaKeyPressTime = now;
                kanaKeyPressCount = 1;
            } else if (kanaKeyPressCount == 1) {
                auto duration = std::chrono::duration_cast<std::chrono::seconds>(now - firstKanaKeyPressTime).count();
                if (duration <= 1) { // 1秒以内に2回目が押された場合
                    std::string command = "osascript \"LatinToHiragana.applescript\" " + inputBuffer;
                    std::cout << "command: " << command << std::endl;
                    system(command.c_str()); // コマンドを実行

                    inputBuffer.clear();
                    std::cout << "AppleScript実行。Current Bufferをリセット" << std::endl;
                } else {
                    std::cout << "1秒以上経過のため、カウントをリセット" << std::endl;
                }
                kanaKeyPressCount = 0; // カウントをリセット
            }
        } else {
            auto it = keyCodeMap.find(keyCode);
            if (it != keyCodeMap.end()) {
                inputBuffer += it->second;
                std::cout << "Current Buffer: " << inputBuffer << std::endl;
            } else {
                inputBuffer.clear();
                std::cout << "Current Bufferをリセット" << std::endl;
            }
        }
    }

    return event;
}

int main() {
    CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventKeyDown), eventCallback, nullptr);
    if (!eventTap) {
        std::cerr << "Failed to create event tap" << std::endl;
        return 1;
    }

    CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
    CGEventTapEnable(eventTap, true);
    CFRunLoopRun();

    return 0;
}