PureBasic GUI チュートリアル
PureBaiscでフォーム(ウィンドウ)を作成、イベントについての記述をするまでのチュートリアルです。
- C:\ProgramData\PureBasic\Examples\Sources\Window.pb
- C:\ProgramData\PureBasic\Examples\Sources\BindEvent.pb
にあるサンプルのようにコード中で生成しても良いのですが、GUIでオブジェクトを配置できたほうが楽なため、フォームデザイナでデザインしてみましょう。
(PureBasicは日本語化した状態で説明します)
フォームデザイナの起動
PureBasicが起動している状態でメニューのフォームから、新規フォームを選択します。
このようにフォームデザイナが開き、新規フォームと共に右側のペインにオブジェクトの一覧やプロパティが表示されます。
フォームに必ず設定するもの
コード中でフォームを判別するには、まずフォームの名前を付けなくてはなりません。
フォームを選択した状態で右ペインに表示されるプロパティから、「Variable」欄を見ます。
デフォルトで「Window_0」などと入力されていますが、これを分かりやすい名前に変更します。
例えばフォームであればfrmプレフィックスをつけて「frmMain」などとすると良いです。
(私がVB6を使っていたときの癖です。「Visual Basic Control Prefix」などと検索すると様々な例があります)
PureBasicでは、フォームやボタンなどのオブジェクト(部品)を全て数字のIDで管理します。
そのため、基本はIDを設定しないと使えません。
しかし、PureBasicには自動的にIDを割り当てる機能があります。
それが、プロパティ欄の「#PB_Any」になります。
チェックを入れたままにしておけば、自動生成のIDで管理されるため、数字と対応するオブジェクトを管理する手間も省けます。
フォームデザイナで生成されたコードを確認してみる
IDEの「フォーム」メニューから、「コード/デザインビューを切り替え」をクリックします。
このようなコードが生成されています。
;
; This code is automatically generated by the FormDesigner.
; Manual modification is possible to adjust existing commands, but anything else will be dropped when the code is compiled.
; Event procedures needs to be put in another source file.
;
Global frmMain
Procedure OpenfrmMain(x = 0, y = 0, width = 600, height = 400)
frmMain = OpenWindow(#PB_Any, x, y, width, height, "", #PB_Window_SystemMenu)
EndProcedure
Procedure frmMain_Events(event)
Select event
Case #PB_Event_CloseWindow
ProcedureReturn #False
Case #PB_Event_Menu
Select EventMenu()
EndSelect
Case #PB_Event_Gadget
Select EventGadget()
EndSelect
EndSelect
ProcedureReturn #True
EndProcedure
フォームデザイナではウィンドウなどを初めとして各種オブジェクトを生成するコードを生成します。
つまりGUIといっても実体はソースコードを生成しているだけです。
そのため、このように切り替えて実際のソースコードを見ることが出来ます。
コードを追記することも出来ますが、フォームデザイナとの辻褄が合わなくなっていきますので、生成されたコードはいじらないことを心がけると良いです。
注意点ですが、このフォームが表示されている状態でコンパイル/実行を行うと、フォームの生成されたコードに対するコンパイルが行われ、フォームを表示することだけしかできません。
別ファイルで書かれているコードは実行対象にならないので、注意してください。
ソースコードを見てみると、Globalでフォームに名付けた「frmMain」という変数が宣言されていることが分かります。
そして、「OpenfrmMain」というプロシージャ(関数)にフォーム(ウィンドウ)を開く処理が書かれています。
frmMainにOpenWindow関数の戻り値を代入する処理になっているため、このプロシージャの実行をするとフォーム(ウィンドウ)が表示され、IDがfrmMainに代入されるのです。
ちなみに、「#PB_Any」にチェックを入れなかった場合は以下のようなコードになります。
Enumeration FormWindow
#frmMain
EndEnumeration
Procedure OpenfrmMain(x = 0, y = 0, width = 600, height = 400)
OpenWindow(#frmMain, x, y, width, height, "", #PB_Window_SystemMenu)
EndProcedure
「Enumeration」というのは列挙型です。
1つしか列挙されていませんが、複数のフォームを用意した際などは同じ名前の列挙型が宣言され、コンパイル時に全てのファイルにまたがってユニークなID(0から続く数字)が適用されるようです。
そのユニークな数値を使用し、各フォームを区別するような実装になります。
フォームを呼び出すコードの書き方
先程作成したフォームを実際に呼び出してみましょう。
フォームに「frmMain」と名付け保存します。
また、以下のコードを「Test」として同じフォルダに保存します。
XIncludeFile "frmMain.pbf"
OpenfrmMain()
以下のような画面になるはずです。
ソースコード中、XIncludeFileで別のファイル(今回はフォーム)をインクルードし、フォーム内の生成されたプロシージャ「OpenfrmMain」を呼んでいます。
この状態で、コンパイル/実行を押してみましょう。
フォーム(ウィンドウ)が一瞬開いて、すぐに閉じてしまったと思います。
これで正常です。
命令の通り、フォームを開き、その後の命令が何も無いので終了してしまったのです。
イベントループの書き方
フォームがすぐに閉じてしまうのでは意味がありません。
GUIでのプログラミングはイベントを処理するためのループが必要です。
以下のようなコードにしてみましょう。
XIncludeFile "frmMain.pbf"
OpenfrmMain()
Repeat
event = WaitWindowEvent()
Until event = #PB_Event_CloseWindow
コード末尾のRepeatに注目です。
これはUntilで書かれた条件になるまでRepeat~Untilで囲われた部分が繰り返し実行されます。
C言語と違い、代入演算子と比較演算子は共通でイコールが1つです。
WaitWindowEvent関数は、イベントが発生するまでコードの実行をブロックします。
そのためフォームが開いたら、何かフォームに動きがあるまで、WaitWindowEvent関数のところで待機状態になり、フォームに動きがあった時点でevent変数に起こったイベント内容が代入されます。
#PB_Event_CloseWindowはフォーム(ウィンドウ)を閉じたことに対するイベントの定数で、一致するまでイベントをループで処理し続けるコードになっています。
コンパイル/実行を押してみましょう。
フォーム(ウィンドウ)が表示されたままになるはずです。
これがフォームを表示するための最低限のコードです。
ボタンの表示方法
フォーム上にボタンを配置してみましょう。
右側のペインから「Button」をドラッグで配置します。選択後、クリックやドラッグでも配置できます。
このようにフォームにボタンを3個配置しました。
プロパティの「Variable」欄でbtnMessage1、btnMessage2、btnMessage3と設定してあります。
また、ボタン上のテキストを表示するために「Caption」欄にも表示内容を入力してあります。
IDEの「フォーム」メニューから、「コード/デザインビューを切り替え」をクリックして生成されたコードを見てみましょう。
;
; This code is automatically generated by the FormDesigner.
; Manual modification is possible to adjust existing commands, but anything else will be dropped when the code is compiled.
; Event procedures needs to be put in another source file.
;
Global frmMain
Global btnMessage1, btnMessage2, btnMessage3
Procedure OpenfrmMain(x = 0, y = 0, width = 600, height = 400)
frmMain = OpenWindow(#PB_Any, x, y, width, height, "", #PB_Window_SystemMenu)
btnMessage1 = ButtonGadget(#PB_Any, 20, 20, 200, 100, "Message1")
btnMessage2 = ButtonGadget(#PB_Any, 20, 150, 200, 100, "Message2")
btnMessage3 = ButtonGadget(#PB_Any, 20, 280, 200, 100, "Message3")
EndProcedure
Procedure frmMain_Events(event)
Select event
Case #PB_Event_CloseWindow
ProcedureReturn #False
Case #PB_Event_Menu
Select EventMenu()
EndSelect
Case #PB_Event_Gadget
Select EventGadget()
EndSelect
EndSelect
ProcedureReturn #True
EndProcedure
OpenfrmMainプロシージャ(関数)にボタンを表示する処理が加わっているのが分かると思います。
そのためボタンも表示できるようになるのです。
イベントを受け取る方法
コードの内容を以下のように変更します。
XIncludeFile "frmMain.pbf"
OpenfrmMain()
Procedure btnMessage1_Events(EventType)
MessageRequester("Title", "Hello World!", #PB_MessageRequester_Ok)
EndProcedure
Repeat
event = WaitWindowEvent()
frmMain_Events(event)
Until event = #PB_Event_CloseWindow
これでイベントを受け取る準備は出来ました。
「btnMessage1_Events」というプロシージャを追加して、イベントループ中で「frmMain_Events」プロシージャを呼ぶようにしています。
そして、重要なのは、フォームデザイナでボタンを選択し、レイアウト中の「Event procedure」欄で「btnMessage1_Events」を選択または入力することです。
ここまで設定したら、「フォーム」メニューから、「コード/デザインビューを切り替え」をクリックして生成されたコードを見てみましょう。
;
; This code is automatically generated by the FormDesigner.
; Manual modification is possible to adjust existing commands, but anything else will be dropped when the code is compiled.
; Event procedures needs to be put in another source file.
;
Global frmMain
Global btnMessage1, btnMessage2, btnMessage3
Declare btnMessage1_Events(EventType)
Procedure OpenfrmMain(x = 0, y = 0, width = 600, height = 400)
frmMain = OpenWindow(#PB_Any, x, y, width, height, "", #PB_Window_SystemMenu)
btnMessage1 = ButtonGadget(#PB_Any, 20, 20, 200, 100, "Message1")
btnMessage2 = ButtonGadget(#PB_Any, 20, 150, 200, 100, "Message2")
btnMessage3 = ButtonGadget(#PB_Any, 20, 280, 200, 100, "Message3")
EndProcedure
Procedure frmMain_Events(event)
Select event
Case #PB_Event_CloseWindow
ProcedureReturn #False
Case #PB_Event_Menu
Select EventMenu()
EndSelect
Case #PB_Event_Gadget
Select EventGadget()
Case btnMessage1
btnMessage1_Events(EventType())
EndSelect
EndSelect
ProcedureReturn #True
EndProcedure
フォーム側のコードでは、「frmMain_Events」プロシージャが生成されています。
先程までのコードでは一切呼び出しをしていませんでしたが、ボタンイベントの実行にはこのプロシージャの呼び出しが必要になります。
eventを引数に取るため、メインループからは取得したイベントを渡し、呼び出します。
自動生成されたコード中では、ボタンに指定した「btnMessage1_Events」プロシージャの呼び出しがEventTypeの引数付きで指定されます。
メインループ中の「frmMain_Events」プロシージャ呼び出しから、ボタンのイベントが発生した際に「btnMessage1_Events」プロシージャの呼び出しまで多段で行われるということです。
btnMessage2, btnMessage3も同様にプロシージャを増やし設定することでイベントの記述をすることが出来ます。
上記のコードでは、btnMessage1をクリックした際にメッセージボックスが表示されるでしょう。
BindEvent / BindGadgetEvent関数でもイベントのバインドが出来るので調べてみるのも良いでしょう。
他のオブジェクトの操作
btnMessage1がクリックされたらメッセージボックスを表示するのではなく、btnMessage2のCaptionを変更してみましょう。
XIncludeFile "frmMain.pbf"
OpenfrmMain()
Procedure btnMessage1_Events(EventType)
SetGadgetText(btnMessage2, "Change Text")
EndProcedure
Repeat
event = WaitWindowEvent()
frmMain_Events(event)
Until event = #PB_Event_CloseWindow
PureBasicでは、フォーム上に配置できる各種オブジェクトを「Gadget」と言います。
ドキュメントのGadgetを見ると様々な関数があり、ガジェットの操作が可能です。
今回は、SetGadgetText関数を用いて、ボタンのテキストを変更しました。
ガジェットのIDに関しては、フォーム側のソース「frmMain.pbf」中にてGlobalで宣言されているので、設定した名前「btnMessage2」でどこからでも呼び出すことが出来ます。
なお、フォーム(ウィンドウ)の操作に関しては関数が違い、SetWindowTitleという関数が存在します。
SetWindowTitle(frmMain, "Change Text")
このように操作することが可能です。
フォームのイベントプロシージャ生成が無効の場合
今まで、フォームで自動生成されたイベントプロシージャ「frmMain_Events」を活用してきました。
イベントプロシージャの生成は、フォームデザイナ上のフォームを選択した状態で、右ペインのレイアウト中の項目で無効にできます。
その場合、生成されるコードは以下のようにイベントプロシージャが含まれません。
;
; This code is automatically generated by the FormDesigner.
; Manual modification is possible to adjust existing commands, but anything else will be dropped when the code is compiled.
; Event procedures needs to be put in another source file.
;
Global frmMain
Global btnMessage1, btnMessage2, btnMessage3
Declare btnMessage1_Events(EventType)
Procedure OpenfrmMain(x = 0, y = 0, width = 600, height = 400)
frmMain = OpenWindow(#PB_Any, x, y, width, height, "", #PB_Window_SystemMenu)
btnMessage1 = ButtonGadget(#PB_Any, 20, 20, 200, 100, "Message1")
btnMessage2 = ButtonGadget(#PB_Any, 20, 150, 200, 100, "Message2")
btnMessage3 = ButtonGadget(#PB_Any, 20, 280, 200, 100, "Message3")
EndProcedure
以下のようにメインコード内にてイベントを取得する必要があります。
XIncludeFile "frmMain.pbf"
OpenfrmMain()
Procedure btnMessage1_Events(EventType)
MessageRequester("Title", "Hello World!", #PB_MessageRequester_Ok)
EndProcedure
Repeat
event = WaitWindowEvent()
Select event
Case #PB_Event_Gadget
Select EventGadget()
Case btnMessage1
btnMessage1_Events(EventType())
EndSelect
EndSelect
Until event = #PB_Event_CloseWindow
大体同じような形でイベントプロシージャを記述することになります。
手間になるのでイベントプロシージャの生成は有効のままにしておきます。
2個のフォームを表示する方法
2個のフォームを使用する方法を考えてみましょう。
XIncludeFile "frmMain.pbf"
XIncludeFile "frmSub.pbf"
OpenfrmMain()
OpenfrmSub()
Procedure btnMessage1_Events(EventType)
MessageRequester("Title", "Hello World!", #PB_MessageRequester_Ok)
EndProcedure
Repeat
event = WaitWindowEvent()
frmMain_Events(event)
frmSub_Events(event)
Until event = #PB_Event_CloseWindow
このような形で、フォームファイルを追加した後に、宣言を追加すれば良いのでは?と思うでしょう。
半分正解、半分不正解です。
実際にコンパイルして実行してみましょう。
フォーム(ウィンドウ)が2個表示されると思います。
しかし、どちらかを閉じると、両方閉じてしまいます。
これはなぜか?
今のメインループでは、PB_Event_CloseWindowが発生したらループから抜けるようになっています。
つまり、どちらかのフォーム(ウィンドウ)を閉じて、PB_Event_CloseWindowイベントが発生してしまうと、ループを抜けてしまいプログラム自体が終了してしまうのです。
よって、フォームの片方を閉じると無用問答で終了してしまいます。
対策を考えてみましょう。
XIncludeFile "frmMain.pbf"
XIncludeFile "frmSub.pbf"
OpenfrmMain()
OpenfrmSub()
Procedure btnMessage1_Events(EventType)
MessageRequester("Title", "Hello World!", #PB_MessageRequester_Ok)
EndProcedure
Repeat
event = WaitWindowEvent()
window = EventWindow()
Select window
Case frmMain
If Not frmMain_Events(event)
Break
EndIf
Case frmSub
If Not frmSub_Events(event)
CloseWindow(frmSub)
EndIf
EndSelect
ForEver
これならどうでしょう?
「frmMain」を親と見立てて、「frmSub」を閉じても終了せず「frmMain」は表示されたまま、「frmMain」を閉じると全部閉じて終了する、という実装です。
ループ内のコードを見てみましょう。
新たにEventWindowという関数を追加しています。
これはイベントが起こったフォーム(ウィンドウ)のIDを取得する関数です。
window変数に結果を保存して、グローバルで宣言されているフォームのID、frmMain、frmSubを使用して動作を振り分けています。
フォーム側で自動生成されるイベントプロシージャを見ると、フォーム(ウィンドウ)が閉じられた際にプロシージャがFalseを返すようになっています。
そのため、If Notで反転させて判定をしています。
つまり、フォーム(ウィンドウ)が閉じられるとIf内のステートメントが実行されるのです。
frmMainが閉じられたときには、Breakが実行されループから抜けます。よって、アプリケーション自体の終了になります。
frmSubが閉じられたときには、CloseWindow関数が実行され、frmSubだけが閉じられます。
frmSubの説明は矛盾しているように聞こえますが、これはこういうものです。
実際にはフォーム(ウィンドウ)の閉じるボタンを押したときにイベントが発火しますが、フォームを閉じるという動作はされません。
今まではイベント発火でループを抜けるようなコードとなっていたため、アプリケーションの終了となり、結果的にフォームを閉じる必要が無かったのです。
今回はfrmSubを閉じてもそのまま実行を続けるアプリケーションを想定しています。
そのため、CloseWindow関数で明示的にフォームを閉じないと表示されたままで、動作していないように見えるコードになってしまうのです。
フォームを開き直す
frmMainは開きっぱなしに出来たけれど、frmSubは閉じたら再度開くことは出来ないの?
出来ます。
では、フォームを開くコードを書き足してみましょう。
XIncludeFile "frmMain.pbf"
XIncludeFile "frmSub.pbf"
OpenfrmMain()
OpenfrmSub()
Procedure btnMessage1_Events(EventType)
MessageRequester("Title", "Hello World!", #PB_MessageRequester_Ok)
EndProcedure
Procedure btnMessage2_Events(EventType)
If Not IsWindow(frmSub)
OpenfrmSub()
Else
SetActiveWindow(frmSub)
EndIf
EndProcedure
Repeat
event = WaitWindowEvent()
window = EventWindow()
Select window
Case frmMain
If Not frmMain_Events(event)
Break
EndIf
Case frmSub
If Not frmSub_Events(event)
CloseWindow(frmSub)
EndIf
EndSelect
ForEver
btnMessage2のプロシージャを追加しました。
※フォームデザイナからbtnMessage2でイベントプロシージャをbtnMessage2_Eventsにするのを忘れないでください。設定しないと動作しません)
このコードでは、btnMessage2を押したときにフォーム(ウィンドウ)を開いているかをIsWindow関数で確認しています。
フォームが存在する場合は、0以外の値(True)を返します。
そのため、If Notでフォームを開く関数「OpenfrmSub」を記述しました。
Elseで続けているのは、もしフォームが表示されている場合は、フォーカスを合わせて最前面にアクティブウィンドウとして持ってくるためです。
SetActiveWindow関数でフォームを指定するとフォーカスが当たり、最前面になります。