GTK+でフォーカスを持たないウィンドウを作る
  C/C++, プログラミング

時計ウィジェットやソフトウェアキーボードなどウィンドウをクリックしてもフォーカスを取得しないウィンドウを作りたいことありますよね。

今日はgtkmmでフォーカスを取得しないウィンドウを作ったときのメモをまとめました。

Gtk::Windowにはset_accept_focus関数があり、これでウィンドウがフォーカスを取得するかどうかのヒントを設定することができます。set_accept_focus関数の引数にfalseを指定するとウィンドウがフォーカスを取得しないようヒントが設定されます。これはあくまでも「ヒント」なのでデスクトップ環境によってはこの指定が無視されてしまうこともあるようです。

set_accept_focus関数ではウィンドウ作成時に初期フォーカスを取得してしまうことを防ぐことはできませんでした。ウィンドウ作成時にフォーカスを取得しないようにするために更にset_focus_on_map関数を呼び出す必要がありました。

サンプルコード

サンプルコードは以下のようになります。 (ただし、以下のサンプルコードは不完全です。どのような問題があって、どう対策が必要なのか追って説明していきます。)

main.cppクリップボードへコピー
#include "NoActivateWindow.h" int main(int argc, char* argv[]) { auto app = Gtk::Application::create(argc, argv); NoActivateWindow window; return app->run(window); }
NoActivateWindow.hクリップボードへコピー
#ifndef NOACTIVATEWINDOW_H_ #define NOACTIVATEWINDOW_H_ #include <gtkmm.h> class NoActivateWindow : public Gtk::Window { public: NoActivateWindow(); virtual ~NoActivateWindow(); protected: Gtk::Button m_button; }; #endif /* NOACTIVATEWINDOW_H_ */
NoActivateWindow.cppクリップボードへコピー
#include <gtkmm.h> #include "NoActivateWindow.h" NoActivateWindow::NoActivateWindow() : m_button("BUTTON") { m_button.set_margin_top(20); m_button.set_margin_right(20); m_button.set_margin_bottom(20); m_button.set_margin_left(20); add(m_button); // // フォーカスを取得しないようにします。 // set_accept_focus(false); // // 起動時にフォーカスを取得しないようにします。 // (set_accept_focusの指定だけでは起動時にウィンドウにフォーカスが当たってしまいます) // set_focus_on_map(false); show_all(); } NoActivateWindow::~NoActivateWindow() { }

動作を確認してみる

Ubuntu(Linux)でビルドして実行してみると以下のようになりました。

ウィンドウをクリックしてもフォーカスを得ることはなく、他のウィンドウがフォーカスを維持していました。

次にWindowsでビルドして実行してみます。プログラムを起動した状態ではフォーカスを取得していません。はじめにメモ帳にフォーカスを当てます。テキスト入力のキャレットが表示されおり、(分かりにくいですが)メモ帳のウィンドウボーダーはアクティブウィンドウを表す水色になっています。

この状態でウィンドウをクリックしてみると…。

ウィンドウはフォーカスを取得していないように見えます。ウィンドウボーダーが水色になっていません。しかし、メモ帳がフォーカスを失ってしまいました。

たしかに、ウィンドウはフォーカスを取得しないようになりましたが、元のウィンドウがフォーカスを失ってしまうのでは意味がありませんよね。

どうして?

Ubuntu(Linux)での動作は問題ありませんが、Windowsでは問題ありです。

ちょっと調べてみましょう。Windowsアプリケーションでフォーカスを取得しないウィンドウを作成に必要な事が2つあります。

  • WM_MOUSEACTIVATEメッセージに対してMA_NOACTIVATEを返す。
  • Win32 API CreateWindowEx関数の拡張ウィンドウスタイルWS_EX_NOACTIVATEを指定する。

GTK+のソースコードを確認したところ、set_accept_focus(false)を呼び出すことによって、WM_MOUSEACTIVATEメッセージ受信時にMA_NOACTIVATEを返す、という振る舞いになっていました。前者はOKですね。

次に、ウィンドウに拡張スタイルWS_EX_NOACTIVATEが設定されているか確認してみます。これはGTK+のソースコードで確認するのは大変なのでSpy++を使って実際にウィンドウに設定されているスタイルを調べます。

Spy++はウィンドウメッセージやウィンドウの情報を確認できる便利なツールです。スパイと言っても怪しいツールではありませんのでご安心ください。Visual Studioに付属しています。Visual Studio Communityにも付属しているので無償で使うことができます。

たとえば、Visual Studio 2017 Communityをインストールすると以下の場所にSpy++がインストールされます。

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\spyxx.exe

Spy++を起動してウィンドウ検索をします。ファインダーツールの照準をドラッグして調べたいウィンドウにドロップすると、ウィンドウの情報が表示されます。今回のサンプルプログラムgtkmm-noactivate.exeの場合は以下のようになります。

ラジオボタンで「プロパティ」が選択されていることを確認して OK をクリックすると、さらに詳細なウィンドウの情報を確認することができます。

プロパティ インスペクターが表示されたら「スタイル」タブに切り替えます。下側に拡張スタイルが一覧表示されています。

残念ながらWS_EX_NOACTIVATEが設定されていません。どうやらこれが原因のようです。

WS_EX_NOACTIVATEを設定する

原因が分かったので、ウィンドウにWS_EX_NOACTIVATE拡張スタイルを追加してみましょう。

ウィンドウの拡張スタイルはウィンドウ作成時(CreateWindowEx)だけでなく、ウィンドウ作成後にSetWindowLong関数を呼び出して変更することもできます。

また、GTK+ではWindows固有のウィンドウハンドル(HWND)を取得するための gdk_win32_window_get_impl_hwnd関数が用意されています。ただし、この関数はgdk/gdkwin32.hで宣言されているのでgtkmm.hをインクルードしているだけでは参照できません。

_WIN32定義の有無でWindowsかどうかを判定し、 Windowsであれば以下の処理を実行するようにプログラムを変更してみましょう。

  • gdk/gdkwin32.hのインクルード
  • gdk_win32_window_get_impl_hwndを呼び出してウィンドウハンドルの取得する
  • SetWindowLongを呼び出してWS_EX_NOACTIVATEを追加する

ウィンドウハンドルを取得するタイミングに注意がしてください。当たり前ですが、ウィンドウが作成された後でないとウィンドウハンドルは取得できません。 Gtk::Windowサブクラスのコンストラクタではまだウィンドウが作成されていません。

ウィンドウが作成されるとon_realize仮想関数が呼ばれますので、これをオーバーライドしてウィンドウハンドルの取得とWS_EX_NOACTIVATEを追加しましょう。

完成したサンプルコードは以下の通りです。Makefileを含む完全なソースコード一式は以下のリンクからダウンロードできます。

main.cppクリップボードへコピー
#include "NoActivateWindow.h" int main(int argc, char* argv[]) { auto app = Gtk::Application::create(argc, argv); NoActivateWindow window; return app->run(window); }
NoActivateWindow.hクリップボードへコピー
#ifndef NOACTIVATEWINDOW_H_ #define NOACTIVATEWINDOW_H_ #include <gtkmm.h> class NoActivateWindow : public Gtk::Window { public: NoActivateWindow(); virtual ~NoActivateWindow(); protected: Gtk::Button m_button; // // オーバーライドを追加しました。 // virtual void on_realize() override; }; #endif /* NOACTIVATEWINDOW_H_ */
NoActivateWindow.cppクリップボードへコピー
#include <gtkmm.h> #ifdef _WIN32 #include <gdk/gdkwin32.h> #endif #include "NoActivateWindow.h" NoActivateWindow::NoActivateWindow() : m_button("BUTTON") { m_button.set_margin_top(20); m_button.set_margin_right(20); m_button.set_margin_bottom(20); m_button.set_margin_left(20); add(m_button); // // フォーカスを取得しないようにします。 // set_accept_focus(false); // // 起動時にフォーカスを取得しないようにします。 // (set_accept_focusの指定だけでは起動時にウィンドウにフォーカスが当たってしまいます) // set_focus_on_map(false); show_all(); } NoActivateWindow::~NoActivateWindow() { } void NoActivateWindow::on_realize() { Gtk::Window::on_realize(); #ifdef _WIN32 HWND hwnd = gdk_win32_window_get_impl_hwnd(get_window()->gobj()); LONG ex_style = GetWindowLongA(hwnd, GWL_EXSTYLE); SetWindowLongA(hwnd, GWL_EXSTYLE, (ex_style | WS_EX_NOACTIVATE)); #endif }

変更したソースコードをビルドしてWindowsで実行してみましょう。ウィンドウをクリックしてもウィンドウがフォーカスを取得することもなく、メモ帳がフォーカスを失うこともなくなりました。

Spy++で拡張スタイルWS_EX_NOACTIVATEが追加されていることも確認できます。

これでWindowsでのフォーカス問題も解決です。