![](https://akitoch.com/wp-content/uploads/2023/10/ArrowRoad.png)
さて、これまでの記事でかなりwxWidgetsの基本的な話をしてきました。具体的には以下の通り。
- MSYS2でwxWidgetsを使う方法 ~GUIアプリケーション開発の第一歩~
-> wxWidgetsライブラリの導入 - C++で最小限のGUIアプリを作る方法 ~wxWidgetsの基礎~
-> 最終目標と、少なくとも理解する必要がありそうな事柄の提示 - GUIアプリ製作で学ぶポリモーフィズム ~~
-> コントロールの配置方法の概説(結局、ポリモーフィズム主体の記事になっちゃったので) - wxWidgetsでイベントに反応できるGUIアプリケーションを作る ~Bind関数の使い方~
-> イベントハンドラを作る方法の解説
※そういえば、第3回記事の副題を追加し忘れてますね。副題を楽しみにされている方がもしいらっしゃったらすみませんが、今後も追加することはないかと思います。今から何か考える気にもなりませんし。まぁ、所詮は適当にやってるブログなので、その辺りはご愛嬌ということで。
※せっかくなのでもう一つ、補足事項みたいなものを。wxWidgets関連の記事も増えてきたので、wxWidgetsというタグを用意しました。過去のwxWidgets関連の記事はそちらからも見られるようになっていると思います。
これまでの内容を応用してもらえれば、簡単なGUIアプリケーションなら難なく作れると思います。
ですが、その”簡単な”という意味は、メニューバー以外にコントロールが1つだけ(例えば、前回作ったように、ボタンが一つだけ)というような意味になります。
もちろん、前回記事までの内容だけでもコントロールが複数個あるようなGUIアプリケーションも作れます。ですが、少し厄介な問題に直面するかもしれないという意味で、「簡単なアプリケーションなら難なく作れる」と説明しています。
基本的な部分はすでに解説していますので、公式リファレンスを見れば普通に解決できるような問題ばかりだと思います。ですが、今回の記事シリーズの最終目標は「電卓のようなアプリケーションを作る」ことです。
ですから、今回はGUIアプリケーションを作る上でのより細かな、だけど無視もしづらいような注意点を解説していきます。痒い所に手が届く様な機能を実装するための解説をするんだと考えてもらえればと思います。
とはいえ、簡単にしか解説しないしできないので、より細かな内容を知りたかったら、結局は「公式リファレンスを読んでください」ということになってしまうのですが。
というわけで、本編に移りましょう。
※この記事シリーズでは、ウィンドウやボタン、入力ボックスといったような、GUIを構成するパーツの総称として「コントロール」という呼び方を採用しています。「ウィジェット」と呼ぶとwxWidgetsというライブラリ名と被ってしまい、ややこしくなりそうだからです。
コントロール、どう配置しようか?
今までの話でコントロールを配置できるようになりましたし、配置したコントロールに対するユーザの操作も検知できるようになりました。
ですが、これまではあまり好ましくはない実装をしていました。それは、MainFrameクラスに、直接コントロールを配置していたことです。
それによって、少し厄介な問題が発生してしまいます。それは、TABキーでコントロール選択を変更できなかったり、コントロールが一つだけだとコントロールの大きさがフレームの大きさと一緒になってしまったりすることです。
それを確認するために、前回作成したソースコードを変更してみましょう。
前回はwxButtonを利用しましたが、その公式リファレンスを読んでみると、第4引数で配置位置を、第5引数でボタンの大きさを指定できることが分かります。
そこで、まずはMainFrame.cppの24行目を次のように変更します。
wxButton* button_plus = new wxButton( this, static_cast<int>(ID::Button::BUTTON1), wxT("plus"), wxPoint( 20, 20 ), wxSize( 60, 15 ) );
その状態でビルドして実行してみてください。おそらく画面いっぱいにボタンが表示されて、前回と変わらない状況になったのではないでしょうか。
次に、ボタンを追加してみます。24行目の次に新しくこんな行を追加してみてください。
wxButton* button_minus = new wxButton( this, static_cast<int>(ID::Button::BUTTON1), wxT("minus"), wxPoint( 80, 20 ), wxSize( 60, 15 ) );
もう一度ビルドして実行してみてください。今度は指定した位置にボタンが表示されたのではないでしょうか。そして、どちらのボタンを押してもアプリケーションが終了するはずです。
ですが、TABキーを押しても選択中のボタンは変わらないと思います。
そこでwxPanelというクラスを利用します。wxPanelを利用すると、今まで説明してきたような厄介な問題を回避できます。
wxPanelの公式リファレンスを読んでみると、wxPanelは子コントロールを管理すること、TABキーでのコントロール移動をサポートすることの2つができるクラスだという説明が書かれています。
今の問題にはおあつらえ向きなクラスですね。
このwxPanelクラスもやはりwxFrame上でnewすることで利用します。
ですが「今まで配置していたコントロールはどうするの?」という疑問が生まれると思います。
wxPanelを利用してGUIアプリケーションを作りこむ
これまではwxFrameに直接コントロールたちを配置していました。ですが、それでは問題があったわけです。つまり、「コントロールが一つだと画面いっぱいに表示される」「TAB移動ができない」の2つですね。
その問題をwxPanelを利用して解決しようとしているというのが現状なわけです。そして、「wxPanelをwxFrame上でnewすればいいのは分かったけど、じゃあ今まで配置していたコントロールはどうやってwxPanelに配置すればいいの?」という疑問を解決しようとしています。
wxPanelを利用するときでも簡単な話で、wxPanel上でコントロールをnewすればいいのです。
wxPanel上でnewするとはどういうことか?と言えば、次の2つの方法のどちらかでコントロールをnewするということになります。
1つ目の方法は、「wxPanelを継承した一つのクラスを作って、そのコンストラクタ上でnewする」という方法です。
例えば、次のようなコードになると思います。
class MyPanel : public wxPanel {
MyPanel();
...他にも関数やら変数やら色々...
};
MyPanel::MyPanel() {
wxButton * button = new wxButton( ... );
...他にもnewやら処理やら...
}
class MyFrame : public wxFrame {
MyFrame();
...他にも関数やら変数やら...
};
MyFrame::MyFrame() {
MyPanel * my_panel = new MyPanel;
...他にも色々と必要な処理...
}
といったような感じです。
2つ目の方法は、wxPanelのポインタをコントロールに登録するという方法です。こちらは本番のコードではなく、プロトタイプ(後で完全に削除して作り直す前提で、最終的なアプリケーションの外観や動作を見るためだけに書くソースコード)に適している(かもしれない)方法です。
またまたwxButtonのリファレンスに戻ってみると、第一引数でwxWindowという型のポインタを渡すようになっています。この引数を、今まではthisと設定していましたが、this以外の値を指定することで親子関係を定義するという方法になります。
具体的には次のようなコードになると思います。
MyFrame::MyFrame() {
wxPanel * my_panel = new wxPanel( this );
wxButton* button1 = new wxButton( my_panel, ... );
wxButton* button2 = new wxButton( my_panel, ... );
...その他必要な処理...
}
先ほどとは違って、新しくクラスを定義する必要がない(1つ目の方法では、wxPanelクラスを継承したMyPanelクラスを作っていました)分だけ、作業量は先ほどの方法よりも少なくて済みます。ですが、どのコントロールが何を親にしているかが分かりにくくなってしまいます。
なので個人的には、基本的には最初の方法の方が分かりやすくて優れていると思います。とはいえ結局は作業時間と可読性の問題なので、すぐに消してしまうコードの場合はこちらの方法の方が優れていると思います。
ということで、これら両方の方法を利用してボタンを配置してみます。
wxPanelをwxFrameの中で作る
まずは作業量が少ない2つ目の方法でwxPanelを作ります。
まずは、wxPanelが定義されているヘッダファイルをインクルードする必要があります。MainFrame.cppの先頭に次のような一文を追加してください。
#include <wx/panel.h>
そして、wxPanel上にwxButtonを配置するようにソースコードを変更します。
上の節でも説明しましたが、wxButtonに親子関係を定義するには、親のポインタが必要です。なので、MainFrame.cppのMainFrame::MainFrame()メンバ関数を次のように変更してください。
MainFrame::MainFrame( const wxString & title, const wxPoint & pos, const wxSize & size )
: wxFrame( nullptr, wxID_ANY, title, pos, size )
{
/* メインウィンドウに必要な要素を作る */
// メニューバーのファイル項目に配置する選択肢を作る。
wxMenu* file_items = new wxMenu(); // 作った選択肢を管理するためのインスタンスを作成
file_items -> Append( static_cast<int>(ID::FileMenu::QUIT), wxT("&Quit"), wxT("Quit this application") ); // Quit という選択肢を作成
// メニューバーを作る
wxMenuBar* menu_bar = new wxMenuBar(); // メニューバーを管理するためのインスタンスを作成
menu_bar -> Append( file_items, wxT("&File") ); // File というメニュー項目を作成
SetMenuBar( menu_bar ); // このウィンドウ上のメニューバーとして、 menu_bar を表示するように伝える
wxPanel * my_panel = new wxPanel( this );
// ボタンを作る
wxPoint plus_position = wxPoint( 20, 20 );
wxSize button_size = wxSize( 60, 15 );
wxPoint minus_position = wxPoint( plus_position.x + button_size.x + 1, plus_position.y );
wxButton* button_plus = new wxButton( my_panel, static_cast<int>(ID::Button::BUTTON1), wxT("plus"), plus_position, button_size );
wxButton* button_minus = new wxButton( my_panel, static_cast<int>(ID::Button::BUTTON1), wxT("minus"), minus_position, button_size );
// メニューバーの Quit が押されたというイベントと onSelectedQuit 関数を結びつける。
// Quit が押されると onSelectedQuit が呼び出されるようにする。
Bind( wxEVT_MENU, &MainFrame::onSelectedQuit, this, static_cast<int>(ID::FileMenu::QUIT) );
// ウィンドウの破棄と、このクラスの quit 関数を紐づける
// ウィンドウの破棄というイベントが発生したときに、このクラスのインスタンスのquit関数が呼び出されるようになる
Bind( wxEVT_CLOSE_WINDOW, &MainFrame::quit, this );
// ボタンが押されたというイベントと、onButton関数を結びつける
Bind( wxEVT_CLOSE_WINDOW, &MainFrame::quit, this );
// ボタンが押されたというイベントとonSelectedQuit関数を結びつける
Bind( wxEVT_BUTTON, &MainFrame::onSelectedQuit, this, static_cast<int>(ID::Button::BUTTON1) );
}
25行目から35行目までが変わりました。
このように変更してから、ビルド、実行していただくと、TAB移動ができるようになっていることを確認できると思います。この変更をもう少し詳しく解説しておきます。
25行目
今回のテーマであるwxPanelのインスタンスを作成しています。そして、その親としてthisポインタを(つまり、MainFrameのポインタを)渡しています。
これによって、MainFrameが親で、my_panelは子として設定されることになります。
27行目から31行目
ここはwxButtonのコンストラクタで指定する変数を設定している部分です。2つのボタン(button_plusとbutton_minus)で利用する座標と大きさを設定しています。
33行目から35行目
ここでwxButtonを作っています。コンストラクタの引数を見てもらえれば分かりますが、すべての引数はここまでに設定した変数になっています。その中でも、特に第一引数に注意してください。
ここまでに説明してきた通り、thisではなく、my_panelとなっています。これによってmy_panelが親で、その子としてbutton_plusとbutton_minusが設定されることになります。
ですから、最終的な親子関係は次のようになります。
[ MainFrame ] <- [ my_panel ] <- [ button_plus, button_minus ]
※ちなみに、前回記事では次のようになっていました。
[ MainFrame ] <- [ button_plus, button_minus ]
この変更のおかげで、コントロールが一つだけであってもフレームいっぱいに表示されるということは無くなりました。実際に、33行目から35行目のどちらかをコメントアウトしてビルド、実行していただければ確認できると思います。
wxPanelを継承したクラスを作る
上の節で、コントロールが1つだけでも大きさや位置を指定できるようになったり、TAB移動できるようになったりしたわけですが、親子関係が見づらいです。
そこで、wxPanelを継承したMainPanelというクラスを新しく作りましょう。しかし、それにあたってやや大きな変更を加えておきます。
追加するファイルはIDs.hpp、MainPanel.hpp、MainPanel.cppという3つのファイルです。そして、MainFrame.hppとMainFrame.cppを変更します。
合計で5つのファイルに追加・修正を施すことになります。
まずは、MainFrame.hppに記述していた一部のコードをIDs.hppに移動させます。IDs.hppというファイルを作成した後、次のような内容に変更してください。
IDs.hpp
#include <wx/utils.h>
// 使いたいIDは、すべてここで定義しておく
namespace ID {
enum class FileMenu : unsigned int
{
QUIT = wxID_HIGHEST + 1,
NUM
};
enum class Button : unsigned int
{
BUTTON1 = (unsigned int)FileMenu::NUM + 1,
NUM
};
}; // ID
勘の良い方なら分かるでしょうが、この追加に伴ってMainFrame.hppを次のように変更します。
MainFrame.hpp
#pragma once
#include "IDs.hpp"
#include <wx/menu.h>
#include <wx/frame.h>
class MainFrame : public wxFrame
{
private:
public:
MainFrame( const wxString & title, const wxPoint & pos, const wxSize & size );
virtual ~MainFrame();
void onSelectedQuit( wxCommandEvent & evt ); // quit を呼び出す
void quit( wxCloseEvent & event );
};
次に、本命であるMainPanelクラスの作成に移ります。まずはMainPanel.hppという名前のファイルとMainPanel.cppという名前のファイルを作って、次のような内容にしてください。
MainPanel.hpp
#include <wx/panel.h>
class MainPanel : public wxPanel {
private:
public:
MainPanel( wxWindow * );
~MainPanel();
};
MainPanel.cpp
#include "IDs.hpp"
#include "MainPanel.hpp"
#include <wx/button.h>
MainPanel::MainPanel( wxWindow * parent )
: wxPanel( parent ) {
// ボタンを作る
wxPoint plus_position = wxPoint( 20, 20 );
wxSize button_size = wxSize( 60, 15 );
wxPoint minus_position = wxPoint( plus_position.x + button_size.x + 1, plus_position.y );
wxButton* button_plus = new wxButton( this, static_cast<int>( ID::Button::BUTTON1 ), wxT("plus"), plus_position, button_size );
wxButton* button_minus = new wxButton( this, static_cast<int>( ID::Button::BUTTON1 ), wxT("minus"), minus_position, button_size );
}
MainPanel::~MainPanel() {}
最後に、MainFrameを変更して、MainPanelを利用するようにします。MainFrame.cppを次のように変更してください。
#include "MainFrame.hpp"
#include "MainPanel.hpp"
#include <wx/frame.h>
#include <wx/menu.h>
#include <wx/statusbr.h>
#include <wx/button.h>
// 委譲コンストラクタで、親クラスである wxFrame に値を渡す
MainFrame::MainFrame( const wxString & title, const wxPoint & pos, const wxSize & size )
: wxFrame( nullptr, wxID_ANY, title, pos, size )
{
/* メインウィンドウに必要な要素を作る */
// メニューバーのファイル項目に配置する選択肢を作る。
wxMenu* file_items = new wxMenu(); // 作った選択肢を管理するためのインスタンスを作成
file_items -> Append( static_cast<int>(ID::FileMenu::QUIT), wxT("&Quit"), wxT("Quit this application") ); // Quit という選択肢を作成
// メニューバーを作る
wxMenuBar* menu_bar = new wxMenuBar(); // メニューバーを管理するためのインスタンスを作成
menu_bar -> Append( file_items, wxT("&File") ); // File というメニュー項目を作成
SetMenuBar( menu_bar ); // このウィンドウ上のメニューバーとして、 menu_bar を表示するように伝える
MainPanel * main_panel = new MainPanel( this );
// メニューバーの Quit が押されたというイベントと onSelectedQuit 関数を結びつける。
// Quit が押されると onSelectedQuit が呼び出されるようにする。
Bind( wxEVT_MENU, &MainFrame::onSelectedQuit, this, static_cast<int>(ID::FileMenu::QUIT) );
// ウィンドウの破棄と、このクラスの quit 関数を紐づける
// ウィンドウの破棄というイベントが発生したときに、このクラスのインスタンスのquit関数が呼び出されるようになる
Bind( wxEVT_CLOSE_WINDOW, &MainFrame::quit, this );
// ボタンが押されたというイベントと、onButton関数を結びつける
Bind( wxEVT_CLOSE_WINDOW, &MainFrame::quit, this );
// ボタンが押されたというイベントとonSelectedQuit関数を結びつける
Bind( wxEVT_BUTTON, &MainFrame::onSelectedQuit, this, static_cast<int>(ID::Button::BUTTON1) );
}
MainFrame::~MainFrame(){}
void MainFrame::onSelectedQuit( wxCommandEvent & evt )
{
Close( true ); // ウィンドウを閉じる
}
void MainFrame::quit( wxCloseEvent & event )
{
Destroy(); // ウィンドウを破棄する
}
追加・修正について少しの解説を
以上で追加・修正は終了です。上の節と同様に、2つのボタンをTAB移動できると思います。そして、どちらのボタンを押してもアプリケーションが終了することも確認できるはずです。
このwxPanelを継承したクラスを新しく作る方法について、もう少し解説しておきます。
解説したいことは、メニューバーについてと、親子関係についてです。
メニューバー
コントロールの一つにメニューバーというものがあります。このプログラムの中でも配置しています。フレーム上部に表示されるFileとかの項目があるやつですね。
このメニューバーはMainPanelクラスではなく、MainFrameクラスに残したままにしています。
その理由は、メニューバーはwxFrameに付属しているものだからです。wxFrameのリファレンスの冒頭には次のようにあります。
A frame is a window whose size and position can (usually) be changed by the user.
It usually has thick borders and a title bar, and can optionally contain a menu bar, toolbar and status bar. A frame can contain any window that is not a frame or dialog.
つまり、フレームはユーザが大きさとか位置を変えたりできるようなウィンドウのことだと(1文目)。そして、そのフレームというやつには、たいていの場合はメニューバーとかツールバー、ステータスバーも付いているものだと(2文目)。(3文目は省略)
今回のプログラムで実装しているメニューバーはSetMenuBar()という関数を利用して実装しています。この関数はwxPanelには実装されておらず、そもそもwxPanel上にメニューバーを配置することはできないようになっています。
※そのような作りになっているのはおそらく、メニューバー(と、ツールバー、ステータスバー)はあくまでもフレームの付属物であって、wxPanelに実装すべきものではないという考え方があるからなんでしょうな。
そんなわけで、メニューバーの作成処理と配置処理はMainFrame上に残しております。
親子関係
親子関係とはいっても、どちらかというと、その作り方についての話です。
すでに、今回のプログラムの親子関係については提示しているわけですが、そのような親子関係を作るためには、MainPanelに「MainFrameが親だぜ」と伝える必要があるわけです。
その方法として、MainPanelのポインタをMainFrameのコンストラクタの引数に渡して伝えるという方法を取っています。MainPanel::MainPanelのparentという引数がそれです。
そのparentを継承元であるwxPanelのコンストラクタに渡すことで親子関係を作っています。
これによって、「2つのボタンを持つ」という新しい機能を持ったパネルを作れているわけです。そして、MainFrameからこのパネルを呼び出すことで、純粋なwxPanelには無かった「2つのボタンを持つ」という機能が追加されたパネルを利用できるようになっています。
このような親子関係を作ることで、親ウィンドウが破棄されたときに、一緒にMainPanelも破棄されるようになります。
wxWindowという、wxWidgetsでの基本クラスの公式リファレンスにはこうあります。
Please note that all children of the window will be deleted automatically by the destructor before the window itself is deleted which means that you don’t have to worry about deleting them manually.
つまり、あるウィンドウの子は自動的にdeleteされるようになっているから、手動でdeleteする必要は無いよと。
そしてもう一つ、Window Deletionというリファレンスでは
Child windows are deleted from within the parent destructor. This includes any children that are themselves frames or dialogs, so you may wish to close these child frame or dialog windows explicitly from within the parent close handler.
こちらによれば、子となっているウィンドウは、親となっているウィンドウのデストラクタを呼ばれたときに、一緒に破棄されるようになっていると。そして、それがフレームやダイアログだったとしてもそうだと。
ということで、MainPanelクラスをwxPanelの一種として管理してもらうことで、親クラス(つまり、MainFrameクラス)が破棄されたときに一緒に破棄してもらおうとしています。
実装の手段としては、MainPanelがwxPanelを継承しないように(つまり、MainPanelのクラス定義時の「public wxPanel」という部分を削除)して、次のようにする方法もあり得ます。特に6行目から7行目に注意してください。
#include "IDs.hpp"
#include "MainPanel.hpp"
#include <wx/button.h>
MainPanel::MainPanel( wxWindow * parent ) {
wxPanel * my_panel = new wxPanel( parent );
// ボタンを作る
wxPoint plus_position = wxPoint( 20, 20 );
wxSize button_size = wxSize( 60, 15 );
wxPoint minus_position = wxPoint( plus_position.x + button_size.x + 1, plus_position.y );
wxButton* button_plus = new wxButton( my_panel, static_cast<int>( ID::Button::BUTTON1 ), wxT("plus"), plus_position, button_size );
wxButton* button_minus = new wxButton( my_panel, static_cast<int>( ID::Button::BUTTON1 ), wxT("minus"), minus_position, button_size );
}
MainPanel::~MainPanel() {}
ですが、こちらの方法ではMainPanelはwxWindowとしては定義されないため、MainFrameが破棄されたとしても、MainPanelが破棄されません。
そのような問題を回避するために、MainPanelにwxPanelを継承させて、wxPanelのコンストラクタに親のポインタを渡しています。
まとめ
今回は主にwxPanelの解説でした。このwxPanel上に第二回でお見せしたような完成予想図に向けてコントロールを配置していくことになります。
そもそもwxPanelの重要な機能として、次の2つの機能があることを説明しました。
- コントロールのTAB移動を可能にする
- 配置されたコントロールを管理する
の2つです。ですが、2つ目に関してはそこまで詳しく説明していません(し、できません)から、ここでは「wxPanelを使えば、コントロールのTAB移動を可能にできるのだ」と考えておいていただければと思います。
そのwxPanelの使い方には2つの方法があって、その方法とは
- wxPanelを継承したクラスを定義する方法
- wxPanelをコントロールに親として指定する方法
の2つでした。
wxWidgetsでGUIアプリケーションを作ろうって記事シリーズはまだ続きそうです。