在Windows应用程序中,当使用多线程时,我知道需要调用主线程来更新GUI组件。这是如何在控制台应用程序中完成的?
例如,我有两个线程,一个主线程和一个辅助线程。辅助线程始终监听全局热键;当按下时,辅助线程会执行一个事件,该事件会访问 win32 api 方法 AnimateWindow。我收到错误,因为只允许主线程执行所述函数。
当“Invoke”不可用时,如何有效地告诉主线程执行该方法?
class Hud
{
bool isHidden = false;
int keyId;
private static IntPtr windowHandle;
public void Init(string[] args)
{
windowHandle = Process.GetCurrentProcess().MainWindowHandle;
SetupHotkey();
InitPowershell(args);
Cleanup();
}
private void Cleanup()
{
HotKeyManager.UnregisterHotKey(keyId);
}
private void SetupHotkey()
{
keyId = HotKeyManager.RegisterHotKey(Keys.Oemtilde, KeyModifiers.Control);
HotKeyManager.HotKeyPressed += new EventHandler<HotKeyEventArgs>(HotKeyManager_HotKeyPressed);
}
void HotKeyManager_HotKeyPressed(object sender, HotKeyEventArgs e)
{
ToggleWindow();
}
private void ToggleWindow()
{
//exception is thrown because a thread other than the one the console was created in is trying to call AnimateWindow
if (isHidden)
{
if (!User32.AnimateWindow(windowHandle, 200, AnimateWindowFlags.AW_VER_NEGATIVE | AnimateWindowFlags.AW_SLIDE))
throw new Win32Exception(Marshal.GetLastWin32Error());
}
else
{
if (!User32.AnimateWindow(windowHandle, 200, AnimateWindowFlags.AW_VER_POSITIVE | AnimateWindowFlags.AW_HIDE))
throw new Win32Exception(Marshal.GetLastWin32Error());
}
isHidden = !isHidden;
}
private void InitPowershell(string[] args)
{
var config = RunspaceConfiguration.Create();
ConsoleShell.Start(config, "", "", args);
}
}
正如 MSDN 上的文档 所说:
以下情况该功能将会失败:
- [...]
- 如果线程不拥有该窗口。 [...]
因此,这里不存在有问题的“主”线程(据我所知,Win32Api 不关心执行程序入口点的女巫线程)。
唯一的条件是您必须在拥有正在设置动画的窗口的线程上执行 AnimateWindow。这就是调用 CreateWindow 的函数,因为它是定义线程/消息循环亲和性的函数。
现在您已经发布了示例代码,您将遇到的问题是您不尝试为任何窗口设置动画...而是尝试为控制台窗口本身设置动画...并且您没有打开它是所有者线程(否则当您在应用程序中创建无限循环时它不会刷新)...因此除非您设法强制窗口在该线程上执行代码,否则不可能调用 AnimateWindow。
控制台窗口实际上由 CSRSS 拥有,这是一个以提升的权限执行的系统进程,无论如何,弄乱它们真的很危险。
自 Windows Vista 起,由于进程保护,甚至无法向此类窗口发送消息,因此以前可能被利用来强制该线程执行代码的任何漏洞现在都应该无法使用。
有关控制台窗口特殊性的详细信息,请参阅 Raymond Chen 博客上的 为什么控制台窗口没有以 Windows XP 为主题? 帖子(来自 microsoft windows shell 团队,因此几乎来自源代码)
我正在考虑后台工作者,但由于你没有 UI 线程,所以这是不可能的(参见这个问题)
使用信号量的“经典”线程方法可能应该可行。使用线程安全队列或集合来存储事件并通过同步对象通知主线程有工作要做。
通常是不是在控制台应用程序中完成。如果您尝试使用 Win32 GUI API,我怀疑您确实应该运行消息循环。
您可以从控制台应用程序调用
Application.Run()
或 Application.Run(ApplicationContext)
来启动新的消息循环。我们的想法是使用 SynchronizationContext.Current
封送回主线程。但是,我还没有设法让它工作......您需要以某种方式强制它将其消息循环注册为当前同步上下文,而我还没有设法说服它这样做:(
让它发挥作用:
public class InvokeMainTest
{
private static SynchronizationContext MainSynchronizationContext;
/// <summary>
/// Has to be called at the start of Application
/// </summary>
public static void InitSyncContext()
{
var synchronizationContext = new SynchronizationContext();
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
MainSynchronizationContext = SynchronizationContext.Current;
}
/// <summary>
/// Execute Code on Main Thread in Console App
/// </summary>
public static void InvokeConsole(Action inner)
{
if (SynchronizationContext.Current == MainSynchronizationContext)
{
inner();
}
else
{
MainSynchronizationContext.Post((o) =>
{
inner();
}, null);
}
}
}
程序.cs
using ConsoleApp1;
InvokeMainTest.InitSyncContext();
Task.Factory.StartNew(() =>
{
InvokeMainTest.InvokeConsole(() =>
{
// Crashes the Application, because its run on the main thread
throw new Exception("test1");
});
}, TaskCreationOptions.LongRunning);
Console.ReadKey();