Win32API | タスクバーに表示されるウィンドウのみ 一覧を取得する | HSP

Win32API | タスクバーに表示されるウィンドウのみ 一覧を取得する | HSP

単にウィンドウの一覧を取得する分には EnumWindows 関数 を使うだけで良いですが、タスクバーに表示されるものだけを取得しようと思うと、そこから色々省かなければなりません。
また、チラッとググって出てきた情報だけだと、完全にタスクバーに表示されるものだけに絞れず、
試行錯誤してようやくできたので、忘れないうちにメモしておこうと思います (^^;
また、もしかしたら環境によって動作が異なるかもしれませんので、思い通りの動作をしなかったらコメント等で教えてください o(_ _)o
自分がテスtした環境は Windows 10 Home です。

ソースコード (HSP)

このプログラムでは以下のモジュールを使用しています。

#include "user32.as"
#include "kernel32.as"

#uselib "psapi.dll"
#func global EnumProcessModules "EnumProcessModules" sptr, sptr, sptr, sptr
#func global GetModuleFileNameEx "GetModuleFileNameExA" sptr, sptr, sptr, sptr

#include "mod_clbk2.hsp"

// コンパイル設定
#runtime "hsp3cl"

// 定数
#define NULL  0
#define FALSE 0
#define TRUE  1

#define _MAX_PATH 260

#define GWL_STYLE   -16
#define GWL_EXSTYLE -20
#define GW_OWNER    0x00000004

#define WS_VISIBLE                0x10000000
#define WS_EX_TOOLWINDOW          0x00000080
#define WS_EX_NOREDIRECTIONBITMAP 0x00200000

#define PROCESS_VM_READ           0x0010
#define PROCESS_QUERY_INFORMATION 0x0400

#define MONITOR_DEFAULTTOPRIMARY 0x00000001

// 
// メイン
// 

labelptr clbk, 2, *ew
EnumWindows stat, 0

mes "\n取得数 : " + i + "\n"

stop

// 
// ウィンドウハンドル取得時のコールバック関数
// 
*ew

// 引数
labelargv argv

arg_hWnd   = argv(0)
arg_lParam = argv(1)

// いらないものをはじく
/* ウィンドウスタイル・拡張スタイルを取得 */
GetWindowLong arg_hWnd, GWL_STYLE
style = stat
GetWindowLong arg_hWnd, GWL_EXSTYLE
exStyle = stat

/* 可視状態かどうか */
if (0 == (style & WS_VISIBLE)) : return 1
 if (0 != (exStyle & WS_EX_NOREDIRECTIONBITMAP)) : return 1
    
  /* タスクバーに表示されているかどうか */
 if (0 != (exStyle & WS_EX_TOOLWINDOW)) : return 1
    
/* オーナーウィンドウを持つかどうか */
GetWindow arg_hWnd, GW_OWNER
if (NULL != stat) : return 1

    
// タイトル取得
GetWindowTextLength arg_hWnd
len = stat
sdim text, len + 1
GetWindowText arg_hWnd, varptr(text), len + 1

// ファイルパス取得
/* プロセス ID 取得 */
processID = NULL
GetWindowThreadProcessId arg_hWnd, varptr(processID)

/* プロセスを開く */
OpenProcess PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processID
hProcess = stat

/* モジュールハンドル 取得 */
hModule = NULL
EnumProcessModules hProcess, varptr(hModule), 4, NULL

/* ファイルパス 取得 */
sdim path, _MAX_PATH
GetModuleFileNameEx hProcess, hModule, varptr(path), _MAX_PATH

/* プロセスを閉じる */
CloseHandle hProcess

// ウィンドウの状態取得
IsIconic arg_hWnd
fIconic = stat
IsZoomed arg_hWnd
fZoomed = stat
IsWindowVisible arg_hWnd
fWindowVisible = stat
IsWindowEnabled arg_hWnd
fWindowEnabled = stat

// ウィンドウの位置・サイズ取得
/* ワークエリア内での座標 */
dim wndPl, 11 // WINDOWPLACEMENT 構造体
wndPl.0 = 4 * 11                                         // wndPl.length = sizeof(WINDOWPLACEMENT)
GetWindowPlacement arg_hWnd, varptr(wndPl)
ptNormalPosition = wndPl.7, wndPl.8                      // ptNormalPosition = wndPl.rcNormalPosition.left, wndPl.rcNormalPosition.top
ptNormalSize     = wndPl.9 - wndPl.7, wndPl.10 - wndPl.8 // ptNormalSize     = wndPl.rcNormalPosition.right - wndPl.rcNormalPosition.left, wndPl.rcNormalPosition.bottom - wndPl.rcNormalPosition.top

/* 位置の補正 */
dim mi, 10 // MONITORINFO 構造体
mi.0 = 10 * 4                     // mi.cbSize = sizeof(MONITORINFO)
MonitorFromWindow arg_hWnd, MONITOR_DEFAULTTOPRIMARY
GetMonitorInfo stat, varptr(mi)
ptNormalPosition.0 += mi.5 - mi.1 // ptNormalPosition.x += mi.rcWork.left - mi.rcMonitor.left
ptNormalPosition.1 += mi.6 - mi.2 // ptNormalPosition.y += mi.rcWork.top  - mi.rcMonitor.top

// 表示
mes "hWnd : " + arg_hWnd
mes "text : " + text
mes "path : " + path
mes "IsIconic : " + fIconic
mes "IsZoomed : " + fZoomed
mes "IsWindowVisible : " + fWindowVisible
mes "IsWindowEnabled : " + fWindowEnabled
mes "ptNormalPosition : (" + ptNormalPosition.0 + ", " + ptNormalPosition.1 + ")"
mes "ptNormalSize     : (" + ptNormalSize.0 + ", " + ptNormalSize.1 + ")"
mes

// カウント
i++

return 1

説明

1.タスクバーに表示されていないものを省く

これが今回の記事のメインテーマとなります。
結論から言いますと、
  • ウィンドウスタイルに WS_VISIBLE を持たないもの
  • 拡張ウィンドウスタイルに WS_EX_TOOLWINDOWWS_EX_NOREDIRECTIONBITMAP を持つもの ※重要
  • オーナーウィンドウを持つもの
を省きます。
なぜ WS_EX_NOREDIRECTIONBITMAP を重要としたかと言うと、この定数に関する情報がググっても全然出てこなかったためです。具体的な理由は後述します。
それ以外の除外条件に関しては、下記のページを参考にさせてもらいました。
なお、上記のページでは、ウィンドウタイトルが空文字列のものも省いているようですが、これをすると本当に表示されているウィンドウなのに取得できない! という事になり得りますので、しないでください。
HSP なら、 

title ""

 としたウィンドウのことを考えれば当然ですよね。

しかし、ここで問題が起きました。デスクトップらしきウィンドウまでが一覧に含まれてしまったのです。
実は上記のページでは、まさにそのデスクトップのウィンドウを、「タイトルが空文字のウィンドウ」として省いていたのです。しかし、先ほど書いたように、デスクトップでなくともタイトルが空文字列のウィンドウは存在し得ります。やはり空文字列のウィンドウを省きたくありません。
ここからが大変な道のりでした…。
自分の環境ではマルチディスプレイだったため、始めは「デスクトップのウィンドウは複数生成されるだろうから、何かしらの親ウィンドウ (オーナーウィンドウ) を持つだろう」と考えました。しかし、これは間違いでした…。よって、オーナーウィンドウを持っているものを弾いているにもかかわらず、取得されてしまいます…。
次に考えたのは、GetDesktopWindow 関数 でした。マルチディスプレイなのにどうやったら複数取得できるんだ!?と思いつつも、どれか一つはウィンドウハンドルが一致するだろうと思い、これで一致するかをチェックし、省こうと試みました。しかし、結果は失敗でした…。ウィンドウハンドルは何れとも一致しませんでした。
さらに今度はウィンドウクラスで弾くことを考えました。自分の環境では "ApplicationFrameWindow" と言う名前でした。しかし、これについて調べてみたところ、これは Windows 8 以降で登場したものらしく、Windows 7 等では使えないようです。現在はまだ「Windows 10 が安定するまでアップグレードしたくない」等でまだ Windows 7 を使い続けている人も少なくないですので、この名前を使ってしまうと問題です。
最終的にたどり着いたのが拡張ウィンドウスタイルでした。はじめは普通のウィンドウスタイルを見ていたのですが、なんと、デスクトップのはずなのに、通常の、タイトルバーが付いてて大きさが変更できて枠がある、ようなウィンドウのウィンドウスタイルだったのです!これは何かあるなと思い、今度は拡張ウィンドウスタイルを見てみました。すると、0x00200000 という見慣れない拡張ウィンドウスタイルが指定されていました。普通のウィンドウスタイルであれば、ググればスグ定数が分かるのですが、これはググっても全然ヒットせず、仕方がないので、手持ちの winuser.h ファイルを参照し、調べてみました。すると、それが最初に法にも述べた WS_EX_NOREDIRECTIONBITMAP だったのです!
この定数は MSDN によると、(以下、MSDN より引用)

The window does not render to a redirection surface. This is for windows that do not have visible content or that use mechanisms other than surfaces to provide their visual.

ざっくり訳すと (自分は英語力が低いので多少間違ってるかもしれませんが (^^;

このウィンドウスタイルが指定されたウィンドウは、外観上、出力先に描画されません。
これは、目に見える中身を持たなかったり、目に見えるものを内部的に与える仕組みを使うウィンドウを作るためです。

ということで、ようは「それ自身は画面上に表示しないけど処理だけはしたい」というようなときに使うようです (^^
これなら、万一デスクトップ以外にこのスタイルが含まれているウィンドウがあるにせよ、画面上に描画されないはずですから、省いてしまっても問題がないと思われます。(もしあったらごめんなさい (^^;
というわけで、この拡張ウィンドウスタイルを持つものも省いたら上手くいきました (^^

2.様々なウィンドウ情報 (タイトル・ファイルパス・最小化/最大化の状態など) の取得

これはそれぞれググれば普通に出てくるので問題はないでしょう (^^

3.ウィンドウ位置の取得

ただ、ウィンドウ位置の取得に関しては様々な落とし穴があるので注意が必要です。
  • GetWindowRect 関数の罠
  • GetWindowPlacement 関数の罠
Win32API を普段から愛用している皆さんなら既にご存知のことかと思われますが、知らない人もいると思うので念のため。と言うか実際、後者は自分も調べるまで知りませんでした (^^;
GetWindowRect 関数の罠 というのは、最小化時に取得すると画面外の座標が得られるというものです。これを終了時に行い、次回起動時に復元するというプログラムを書いてしまうと、終了時に最小化したままだと、次回に画面上にウィンドウが出てきてくれない!という事になります (^^;
今回はそういう目的ではないですが、できれば、通常状態 (最小化も最大化もされていない時) の位置が知りたいものです。そこで使われるのが GetWindowPlacement 関数です。

GetWindowPlacement 関数の罠 というのは、実は、この関数で得られる座標はスクリーン座標ではないというものです。
前者の問題からこの関数を使ったことがある方の中には、「え??いや普通にスクリーン座標でしょ??この関数で取得した値を座標として使ったけど何も問題が起きなかったよ??」という人がいらっしゃるかもしれません。(つい数日前までの自分です (^^;
実は、スクリーン座標ではなく、ワークエリア内での座標なんです。


上記のページではウィンドウ位置を復元することが目的なので、SetWindowPlacement 関数を使えばよいという結論に至っていますが、こっちは本当に座標が知りたいので、ワークエリアの座標を取得してから補正する必要があります。

これは人と状況によると思いますが、ディスプレイが1つしかない環境しか想定しないなら、

#define SPI_GETWORKAREA 0x00000030
// SystemParametersInfo(SPI_GETWORKAREA, 0, &rect, 0);
SystemParametersInfo SPI_GETWORKAREA, 0, varptr(rect), 0

で良いですし、マルチディスプレイにちゃんと対応したいのであれば、ウィンドウハンドルを用いて MonitorFromWindow 関数でどのモニターにいるかを取得し、そのモニターの情報から座標を修正します。

はじめ、先ほど取得した座標から MonitorFromPoint 関数を使って取得したほうが良いのではないかと思ったのですが、よく考えたら、先ほど取得した値はスクリーン座標で違うモニターを取得してしまう可能性がありますよね (^^;
というわけでウィンドウハンドルから取得したほうが確実です。
最初、最小化時に問題が起きるのでは?と疑いましたが、MONITOR_DEFAULTTOPRIMARY を MONITOR_DEFAULTTONULL に修正したうえでテストしてみても大丈夫だったので平気みたいです。(ちゃんとテストした訳ではないのでもしダメだったらごめんなさい (^^;

雑談

先日の記事でも書きましたが、今、あるソフトウェアの製作を考えています。
いままでデザインとかあまり拘ってこなかったのですけど、今回は見た目がかわいらしいソフトウェアを作ろうと考えています (^^
プログラミングしてない人から見たら「かわいいものを作るためにこんな面倒くさいことをしてるのか」と思われるかもしれませんが、自分はプログラミングも好きなので、作ってる最中はプログラミング自体を楽しんで、完成したらソフトウェアで楽しむ、と言う感じにしたいです (^^
ではまたいつか~ (^^ ノシ
↓ブログランキング投票お願いします (^^


「Win32API | タスクバーに表示されるウィンドウのみ 一覧を取得する | HSP」への3件のフィードバック

  1. いいですね! Windows7ですけど、
    タスクバーにある開いてるプログラムの一覧が全部表示されました。
    いろんなことに活用できそうです^^

  2. > フェルミウム湾さん
    ありがとうございます!
    Windows 7 でもう動いてくれてよかったです (^^

  3. はじめまして。

    「プログラミングのメモ帳」のブログ管理者です。

    今日、ウインドウ・スタイルの WS_EX_NOREDIRECTIONBITMAP の事を知りました。
    確かに HSP で空文字列をセットしたタイトルバーを容易に作れますね。

    お勉強になりましたよ。
    良い情報をありがとうございます。

コメントは受け付けていません。