Question

開発メモ その38 プロセスIDからUWPAppのHWNDは取得できるか?からの追加調査というか本題。
ウィンドウハンドルが取得できれば、たいていのことはできるのがWindows。
WinFormsでもWPFでも、結局かゆいところはWin32API様を三顧の礼で迎え入れる必要がある。

で、サンドボックス環境で動作するUWPアプリに対して、ウィンドウハンドルを経由して操作ができるか。
これができるかどうかは結構大きい。

Getting HWND off of CoreWindow object in UWPにで、Microsoftの人間がこのようにコメントしている。

Please note there are no supported APIs for UWP that accept an HWND. Any API you call will fail Windows Store certification, and even if you avoid the Windows Store (eg, side-load or go through an Enterprise deployment) there is no guarantee the app will work in the future.

パッと見、Win32APIの呼び出しがダメ、みたいに見えるけど、よく読むと、**(APIを呼び出すと)ストア認証に失敗する** って言っているだけで、APIそのものの成否ついては言及していない。
なので、試してみた。

Answer

一部成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using DWORD = System.UInt32;

namespace ConsoleApplication1
{
class Program
{
delegate bool Win32Callback(IntPtr hWnd, ref IntPtr lParam);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool EnumWindows(Win32Callback lpEnumFunc, ref IntPtr lParam);

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

[DllImport("user32.dll", EntryPoint = "MapVirtualKeyA")]
static extern int MapVirtualKey(int wCode, int wMapType);

[DllImport("user32.dll", SetLastError = true)]
static extern uint SendInput(uint cInputs, KEYBOARDINPUT[] inputs, int cbSize);

[DllImport("user32.dll")]
static extern int SetForegroundWindow(IntPtr hWnd);

const uint INPUT_KEYBOARD = 1;

const uint KEYEVENTF_KEYDOWN = 0;

const uint KEYEVENTF_KEYUP = 2;

const ushort VK_DELETE = 0x2E;

const int KBD_UNICODE = 0x0004;

[StructLayout(LayoutKind.Sequential)]
private struct KEYBOARDINPUT
{
public uint type;
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public uint dwExtraInfo;
public uint unused1;
public uint unused2;
}

static readonly uint GW_HWNDNEXT = 2;

static readonly int GWL_HWNDPARENT = -8;

static readonly IntPtr NULL = System.IntPtr.Zero;

static void Main(string[] args)
{
var hTarget = IntPtr.Zero;
var ps = System.Diagnostics.Process.GetProcesses().Where(process => process.ProcessName == "Calculator");
if (!ps.Any())
return;

var calc = ps.First();
var pid = calc.Id;

Console.WriteLine("プロセス名: {0}", calc.ProcessName);
Console.WriteLine("ID: {0}", calc.Id);

// EnumWindowsProc で探索対象のプロセスIDを設定
TargetProcessId = (uint)pid;

var proc = new Win32Callback(EnumWindowsProc);

EnumWindows(proc, ref hTarget);

Console.WriteLine("HWND: {0}", hTarget.ToString("X"));

if (hTarget == NULL)
{
Console.WriteLine("EnumWindows fails");
return;
}

SendDelKey(hTarget);
}

private static void SendDelKey(IntPtr hwnd)
{
// フォアグラウンドに移動
if (SetForegroundWindow(hwnd) == 0)
{
Console.WriteLine("SetForegroundWindow fails");
return;
}

// Delキーを送って、電卓の数字を0にする
KEYBOARDINPUT[] inputs;
uint retVal;
inputs = new KEYBOARDINPUT[2];
inputs[0].type = INPUT_KEYBOARD;
inputs[0].wVk = VK_DELETE;
inputs[0].wScan = (ushort)MapVirtualKey(VK_DELETE, 0);
inputs[0].dwFlags = (KEYEVENTF_KEYDOWN | KBD_UNICODE);

inputs[1].type = INPUT_KEYBOARD;
inputs[1].wVk = VK_DELETE;
inputs[1].wScan = (ushort)MapVirtualKey(VK_DELETE, 0);
inputs[1].dwFlags = (KEYEVENTF_KEYUP | KBD_UNICODE);

var cbSize = Marshal.SizeOf<KEYBOARDINPUT>();
if (SendInput(2, inputs, cbSize) == 0)
{
Console.WriteLine("SendInput fails");
return;
}
}

private static uint TargetProcessId;

static bool EnumWindowsProc(IntPtr hWnd, ref IntPtr lParam)
{
var hChild = IntPtr.Zero;
var buf = new StringBuilder(1024);
GetClassName(hWnd, buf, buf.Capacity);
switch (buf.ToString())
{
case "ApplicationFrameWindow":
hChild = FindWindowEx(hWnd, IntPtr.Zero, "Windows.UI.Core.CoreWindow", null);
DWORD processId;
GetWindowThreadProcessId(hChild, out processId);
if (TargetProcessId == processId)
{
lParam = hChild;
return false;
}
break;
default:
break;
}
return true;
}

}
}

電卓に対してDELキーを送信し、数字を消すだけ。
でも、電卓はSetForegroundWindowで前面には来たけど、SendInputが失敗する。
Win7の電卓アプリで、似たようなコードが成功するかは確認済み。

Supplement

結局、Windows 10 Universal App Key Press Simulation, what OS Context?という記事で、Microsoftの人間が

There is no way to synthesize input in a Universal app. The SendInput API (and equivalents) that InputSimulator uses to inject input is available only to classic Win32 desktop apps, not to Windows Runtime apps.

と発言しているから無理なんでしょう。
全てのWin32APIが無効、というわけではないが、入出力系は全滅かもしれない。