启动外部进程后 WPF ContextMenu 仍然可见

WPF ContextMenu still visible after launching an external process

我正在从 ContextMenu 启动外部应用程序,我 必须 在目标应用程序 运行 时阻止源应用程序。为此,我使用 Process.WaitForExit() 来避免源应用程序响应事件。

问题是上下文菜单仍然在目标应用程序之前。让我们看一个简单的例子:

这是我用于示例的代码。

    public MainWindow()
    {
        InitializeComponent();

        this.ContextMenu = new ContextMenu();

        MenuItem menuItem1 = new MenuItem();
        menuItem1.Header = "Launch notepad";
        menuItem1.Click += MyMenuItem_Click;
        this.ContextMenu.Items.Add(menuItem1);
    }

    void MyMenuItem_Click(object sender, RoutedEventArgs e)
    {
        Process p = new Process();
        p.StartInfo.FileName = "notepad.exe";
        p.StartInfo.CreateNoWindow = false;
        p.Start();
        p.WaitForExit();
        p.Close();
    }

如何让上下文菜单在目标应用程序显示之前消失?

注意:这不会阻止主应用程序,因此如果有必要,此答案对您不起作用

ui 线程被阻塞,因此无法取消绘制上下文菜单。为什么你能在其他程序上看到它可能与它在屏幕上的绘制方式有关。如 Process.WaitForExit docs

中所述

WaitForExit() makes the current thread wait until the associated process terminates. It should be called after all other methods are called on the process. To avoid blocking the current thread, use the Exited event.

因此您需要将代码更改为

void MyMenuItem_Click(object sender, RoutedEventArgs e)
    {
        Process p = new Process();
        p.StartInfo.FileName = "notepad.exe";
        p.StartInfo.CreateNoWindow = false;
        p.EnableRaisingEvents = true;
        p.Exited += new EventHandler(myProcess_Exited);
        p.Start();
    }

然后

private void myProcess_Exited(object sender, System.EventArgs e)
    {
       //Do whatever logic you have for  when the program exits
    }

经过一些测试,这似乎工作正常:

void LaunchAndWaitForProcess(object sender, RoutedEventArgs routedEventArgs)
{
    this.ContextMenu.Closed -= LaunchAndWaitForProcess;
    Process p = new Process();
    p.StartInfo.FileName = "notepad.exe";
    p.StartInfo.CreateNoWindow = false;
    p.Start();
    p.WaitForExit();
    p.Close();
}
void MyMenuItem_Click(object sender, RoutedEventArgs e)
{
    this.ContextMenu.Closed += LaunchAndWaitForProcess;
}

因为(在评论中)你说你正在使用 ICommands,我猜你在 XAML 中绑定它们并且不想丢失它。

一种通用的方式(丑陋,并且不允许您将 CanExecute 绑定到菜单项的 Enabled 状态,但我能在这么短的时间内找出最不丑陋的方式time) 可以将它们绑定到另一个 属性(例如:Tag),同时也绑定到单个 Click 处理程序。类似于:

在XAML中:

<MenuItem Header="Whatever" Tag="{Binding MyCommand}" Click="MenuItemsClick"         
<MenuItem Header="Other Item" Tag="{Binding OtherCommand}" Click="MenuItemsClick" />

在代码隐藏中:

private ICommand _launchCommand;
private object _launchCommandParameter;

void ExecuteContextMenuCommand(object sender, RoutedEventArgs routedEventArgs)
{
   this.ContextMenu.Closed -= ExecuteContextMenuCommand;
   if(_launchCommand != null && _launchCommand.CanExecute(_launchCommandParameter))
     _launchCommand.Execute(_launchCommandParameter);
}

void MenuItemsClick(object sender, RoutedEventArgs e)
{
    var mi = (sender as MenuItem);
    if(mi == null) return;
    var command = mi.Tag as ICommand;
    if(command == null) return;
    _launchCommand = command;
    _launchCommandParameter = mi.CommandParameter;
    this.ContextMenu.Closed += ExecuteContextMenuCommand;
}

一种可能的解决方案是在菜单关闭时启动进程:

bool _start;

public MainWindow()
{
    InitializeComponent();

    ContextMenu = new ContextMenu();
    var menuItem = new MenuItem() { Header = "Launch notepad" };
    menuItem.Click += (s, e) => _start = true;
    ContextMenu.Items.Add(menuItem);
    ContextMenu.Closed += (s, e) =>
    {
        if (_start)
        {
            _start = false;
            using (var process = new Process() { StartInfo = new ProcessStartInfo("notepad.exe") { CreateNoWindow = false } })
            {
                process.Start();
                process.WaitForExit();
            }
        }
    };
}

经过测试,似乎可行,但我怀疑长时间阻塞 UI 线程的想法是否是个好主意。

基于 Jcl 给出的伟大想法...

我找到了一个使用自定义 MenuItems 的简单解决方案。它会延迟 MenuItem.Click() 事件,直到父 ContextMenu 关闭。

class MyMenuItem : MenuItem
{
    protected override void OnClick()
    {
        ContextMenu parentContextMenu = Parent as ContextMenu;
        parentContextMenu.Closed += ContextMenu_Closed;
        parentContextMenu.IsOpen = false;
    }

    void ContextMenu_Closed(object sender, RoutedEventArgs e)
    {
        ContextMenu parent = Parent as ContextMenu;
        parent.Closed -= ContextMenu_Closed;
        base.OnClick();
    }
}

阻塞 UI 线程(绘图,Window 交互)会产生可怕的用户体验:看起来应用程序被冻结了,实际上是这样。考虑到限制,我会这样做:

void MyMenuItem_Click(object sender, RoutedEventArgs e) {
    Process p = new Process();
    p.StartInfo.FileName = "notepad.exe";
    p.StartInfo.CreateNoWindow = false;
    Title = "Waiting for Notepad to be closed.";
    executeBlocking(p, () => {
        Title = "Finished work with Notepad, you may resume your work.";
    });
}

void executeBlocking(Process p, Action onFinish) {
    IsEnabled = false;
    BackgroundWorker processHandler = new BackgroundWorker();
    processHandler.DoWork += (sender, e) => {
        p.Start();
        p.WaitForExit(); // block in background
        p.Close();
    };
    processHandler.RunWorkerCompleted += (sender, e) => {
        IsEnabled = true;
        onFinish.Invoke();
    };
    processHandler.RunWorkerAsync();
}

禁用 Window 本身 (IsEnabled = false) 将实现 "I must block the source application" 通过不让用户与您的应用交互,而不是移动, 调整大小并关闭它。如果你也需要阻止退出,你可以这样做:

InitializeComponent();
Closing += (sender, e) => {
    if (!IsEnabled) {
        MessageBox.Show("Sorry, you must close Notepad to exit the application");
        e.Cancel = true;
    }
};

对用户来说,表明您正在等待记事本也是一种普遍的礼貌,一旦完成(关闭)您的应用程序将再次可用,我在 window Title 为简单起见,并且由于演示应用程序中缺少任何控件。