時計ウィジェットやソフトウェアキーボードなどウィンドウをクリックしてもフォーカスを取得しないウィンドウを作りたいことありますよね。
今日は 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 でのフォーカス問題も解決です。