GTK+でフォーカスを持たないウィンドウを作る

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

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

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

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

サンプルコード

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

main​.cpp
#include "No​Activate​Window​.h" int main(int argc, char* argv[]) { auto app = Gtk::Application::create(argc, argv); No​Activate​Window window; return app->run(window); }
No​Activate​Window​.h
#ifndef NOACTIVATEWINDOW_H_ #define NOACTIVATEWINDOW_H_ #include <gtkmm​.h> class No​Activate​Window : public Gtk::Window { public: No​Activate​Window(); virtual ~No​Activate​Window(); protected: Gtk::Button m_button; }; #endif /* NOACTIVATEWINDOW_H_ */
No​Activate​Window​.cpp
#include <gtkmm​.h> #include "No​Activate​Window​.h" No​Activate​Window::No​Activate​Window() : 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(); } No​Activate​Window::~No​Activate​Window() { }

動作を確認してみる

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

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

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

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

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

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

どうして?

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

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

  • WM_MOUSEACTIVATE メッセージに対して MA_NOACTIVATE を返す。
  • Win32 API Create​Window​Ex 関数の拡張ウィンドウスタイル 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 拡張スタイルを追加してみましょう。

ウィンドウの拡張スタイルはウィンドウ作成時 Create​Window​Ex だけでなく ウィンドウ作成後に Set​Window​Long 関数を呼び出して変更することもできます。

また 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 を呼び出してウィンドウハンドルの取得する
  • Set​Window​Long を呼び出して WS_EX_NOACTIVATE を追加する

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

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

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

main​.cpp
#include "No​Activate​Window​.h" int main(int argc, char* argv[]) { auto app = Gtk::Application::create(argc, argv); No​Activate​Window window; return app->run(window); }
No​Activate​Window​.h
#ifndef NOACTIVATEWINDOW_H_ #define NOACTIVATEWINDOW_H_ #include <gtkmm​.h> class No​Activate​Window : public Gtk::Window { public: No​Activate​Window(); virtual ~No​Activate​Window(); protected: Gtk::Button m_button; // // オーバーライドを追加しました。 // virtual void on_realize() override; }; #endif /* NOACTIVATEWINDOW_H_ */
No​Activate​Window​.cpp
#include <gtkmm​.h> #ifdef _WIN32 #include <gdk​/gdkwin32.h> #endif #include "No​Activate​Window​.h" No​Activate​Window::No​Activate​Window() : 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(); } No​Activate​Window::~No​Activate​Window() { } void No​Activate​Window::on_realize() { Gtk::Window::on_realize(); #ifdef _WIN32 HWND hwnd = gdk_win32_window_get_impl_hwnd(get_window()->gobj()); LONG ex_style = Get​Window​Long​A(hwnd, GWL_EXSTYLE); Set​Window​Long​A(hwnd, GWL_EXSTYLE, (ex_style | WS_EX_NOACTIVATE)); #endif }

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

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

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

この記事を共有しませんか?