C#编译错误:"在窗口句柄创建之前,无法对控件调用Invoke或BeginInvoke。"

8
我刚刚发布了一个关于如何让委托更新另一个窗体上的文本框的问题。就在我认为使用Invoke得到了答案时...发生了这件事。这是我的代码:
主窗体代码:
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.IO;
using System.Data.OleDb;
using System.Collections.Specialized;
using System.Text;
using System.Threading;

delegate void logAdd(string message);

namespace LCR_ShepherdStaffupdater_1._0
{
    public partial class Main : Form
    {
        public Main()
        {
            InitializeComponent();
        }

        public void add(string message)
        {
            this.Log.Items.Add(message);
        }
        public void logAdd(string message)
        {   /////////////////////////// COMPILER ERROR BELOW ///////////
            this.Invoke(new logAdd(add), new object[] { message }); // Compile error occurs here     
        }////////////////////////////// COMPILER ERROR ABOVE ///////////

        private void exitProgramToolStripMenuItem_Click(object sender, EventArgs e) 
        {
            Application.Exit(); 
        }
        private void aboutToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            Form aboutBox = new AboutBox1(); 
            aboutBox.ShowDialog(); 
        }

        private void settingsToolStripMenuItem_Click(object sender, EventArgs e)
        {
        }

        private void settingsToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            settingsForm.settings.ShowDialog();
        }

        private void synchronize_Click(object sender, EventArgs e)
        {
            string message = "Here my message is"; // changed this
            ErrorLogging.updateLog(message);  // changed this
        }

    }

    public class settingsForm 
    {
        public static Form settings = new Settings();
    }

}

记录类代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LCR_ShepherdStaffupdater_1._0
{
    public class Logging
    {
        static Main mainClass = new Main();
        static logAdd logAddDelegate;

        public static void updateLog(string message)
        {
            logAddDelegate = mainClass.logAdd;
            logAddDelegate(message);
        }
    }
}

编译错误:
InvalidOperationException未处理 - 在窗口句柄创建之前,不能在控件上调用Invoke或BeginInvoke。
我已经尝试在Log项上创建一个句柄,但没有起作用。问题是我不知道自己在做什么,而且我已经通过Google广泛搜索,只找到了模糊的答案。
请告诉我如何在调用此委托之前创建句柄。顺便说一下,给我一些让这段代码更简单的方法。例如,我不想有两个Add函数……我不得不这样做是因为我找不到从Logging类调用的项来调用。有没有更好的方法来完成我需要做的事情?
谢谢!!!
编辑:
我的项目相当大,但这些是导致此特定问题的唯一项目。
Log是我的RichTextBox1(Log.Items.Add(message)),我将其重命名为Log,以便更容易重新输入。
我从不同的表单中调用updateLog(message)...虽然无论我从哪里调用updateLog(message),它都会给我这个错误)
你们必须让我更简单地理解事物,并提供示例。我不理解你们在这里说的一半......我不知道如何使用方法和句柄进行工作。我已经对它进行了大量研究......
第二次编辑:
我相信我已经找到了问题,但不知道该如何解决。
在我的日志记录类中,我使用以下代码创建mainClass:
static Main mainClass = new Main();
我正在创建一个完全新的Main()蓝图副本,包括Log(我正在尝试更新的richtextbox)
当我调用updateLog(message)时,我相信我正在尝试更新Main()的第二个实体上的Log(richtextbox),也就是mainClass。当然,这样做会抛出此异常,因为我甚至没有看到当前正在使用的Main的副本。
这就是我想要的,感谢其中一个给出答案的人:
Main mainClass = Application.OpenForms.OfType<Main>().First();
logAddDelegate = mainClass.logAdd; 
logAddDelegate(message);

我需要创建mainClass,但不使用new()运算符,因为我不想创建一个新的表单蓝图,我希望能够编辑当前的表单。

尽管上面的代码不起作用,我甚至找不到Application。那是C#语法吗?

如果我能让上面的代码运行起来,我认为我可以解决我的问题,并在寻找答案几个小时后终于解决这个问题。

最终编辑:

由于下面的用户之一,我弄清楚了。这是我的更新后的代码:

主表单代码:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.IO;
using System.Data.OleDb;
using System.Collections.Specialized;
using System.Text;
using System.Threading;

delegate void logAdd(string message);

namespace LCR_ShepherdStaffupdater_1._0
{
    public partial class Main : Form
    {
        private static Main mainFormForLogging;
        public static Main MainFormForLogging
        {
            get
            {
                return mainFormForLogging;
            }
        }

        public Main()
        {
            InitializeComponent();
            if (mainFormForLogging == null)
            {
                mainFormForLogging = this;
            }
        }

        public void add(string message)
        {
            this.Log.Items.Add(message);
        }
        public void logAdd(string message)
        {
            this.Log.BeginInvoke(new logAdd(add), new object[] { message });
        }

        private void exitProgramToolStripMenuItem_Click(object sender, EventArgs e) 
        {
            Application.Exit(); 
        }
        private void aboutToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            Form aboutBox = new AboutBox1(); 
            aboutBox.ShowDialog(); 
        }

        private void settingsToolStripMenuItem_Click(object sender, EventArgs e)
        {
        }

        private void settingsToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            settingsForm.settings.ShowDialog();
        }

        private void synchronize_Click(object sender, EventArgs e)
        {
            add("test");
            Logging.updateLog("testthisone");
            //DatabaseHandling.createDataSet();
        }

    }

    public class settingsForm 
    {
        public static Form settings = new Settings();
    }

}

日志记录类代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LCR_ShepherdStaffupdater_1._0
{
    public class Logging
    {

        static Main mainClass = Main.MainFormForLogging;
        static logAdd logAddDelegate;

        public static void updateLog(string message)
        {
            logAddDelegate = mainClass.logAdd;
            logAddDelegate(message);
        }
    }
}

4
你确定这是编译错误吗?对我来说更像是运行时异常... - Jon Skeet
你确定 mainClass 是你正在查看的 Main 实例吗?有可能你有两个实例 - 一个已被创建,另一个未被创建? - Jon B
伙计们...我自己也不确定...这部分让我感到非常困惑,我几乎不知道发生了什么,哈哈 - OneShot
我刚刚解决了一件事情,但是五分钟之后又出现另一个问题。如果我能够得到一个好的例子,我就知道别人在说什么了。 - OneShot
只是为了快速澄清一些事情:编译时错误意味着您按下构建按钮(默认为Ctrl-Shift-B?),然后出现错误。运行时错误意味着您构建了代码,并且没有出现错误。然后当您运行它时,窗口开始弹出,突然出现了错误,通常会将代码行用黄色标记出来。 - GWLlosa
显示剩余2条评论
9个回答

13

我曾经使用以下方法解决过这个问题:

private void invokeOnFormThread(MethodInvoker method)
{
    if (IsHandleCreated)
         Invoke(new EventHandler(delegate { method(); }));
    else
        method();
}

使用invokeOnFormThread代替Invoke。如果已经创建了句柄,它只会使用表单的线程,否则将使用调用者的线程。


我会尝试将其转换为我的代码...但我不确定所有这些的作用以及为什么我应该这样做。到目前为止,我还无法弄清如何使用它。 - OneShot
这很聪明。它可以让逻辑测试在没有表单的情况下运行。 - Rick Minerich
这是处理Invoke的标准惯用语。是最正确的答案。 - Beeeaaar

12

好的,我要重新开始。

为了理解发生了什么,你需要了解.NET和Windows之间的关系。.NET运行在Windows上,并封装了许多本地的Win32概念,如窗口、列表视图、编辑框(标准文本框的Win32名称)。这意味着您可以拥有一个有效的.NET文本框或表单实例,但是还没有该项的底层Windows版本(EditBox或Window)。当HandleCreated为true时,该项的Windows版本将被创建。

你遇到问题是因为某些事情导致在Form的Window被创建之前调用了logAdd方法。这意味着在实例化Form实例后但在创建Window句柄之前,在启动期间的某个地方,某些东西试图调用logAdd。如果在logAdd中添加断点,您应该能够看到是什么在进行此调用。你会发现,调用是在你的logger类中创建的Main实例上而不是实际正在运行的Main实例上进行的。由于logger实例从未被显示,所以窗口句柄未被创建,因此您会出现错误。

应用程序的一般运行方式是在启动方法中调用Application.Run(new Main()),通常在Program类中调用Main。你需要让logger指向这个main实例。

有几种方法可以获取表单的实例,每种方法都有自己的注意事项,但为简单起见,你可以将该实例从Main类本身公开出来。例如:

public partial class Main : Form
{
    private static Main mainFormForLogging;
    public static Main MainFormForLogging
    {
        get
        {
            return mainFormForLogging;
        }
    }

    public Main()
    {
        InitializeComponent();

        if (mainFormForLogging == null)
        {
            mainFormForLogging = this;
        }
    }

    protected void Dispose(bool disposing)
    {
         if (disposing)
         {
             if (this == mainFormForLogging)
             {
                mainFormForLogging = null;
             }
         }

         base.Dispose(disposing);
    }
}

不,你的新主函数与此有关。看一下我刚刚添加的代码示例,你可以参考其中的解决方法。 - Jeff Yates
没问题。之前用术语把你埋没了,对此我很抱歉——有时候很难在适当的技术水平上回答问题,当我无法表达我的意思时,这让我感到沮丧(这是我的错,不是你的)。我很高兴能够帮助你。继续前进,祝你好运。 - Jeff Yates
这个答案就是我本来想写的,如果我能够清晰地表达的话。如果有人阅读我的答案有困难,那就阅读这个吧。 :) - GWLlosa
1
+1 针对添加断点的提示,这帮助我解决了问题。有时候我会忘记最显而易见的事情... - Miel
+1 感谢所有为这个答案做出贡献的人。正是我需要解决一个大问题的东西。非常感谢。 - JK.
显示剩余2条评论

2
当出现此错误时,几乎总是意味着您尝试在控件或表单实际创建之前对其进行操作。
在WinForms中,GUI元素有两个半独立的生命:作为内存中的类和作为操作系统中的实体。因此,可能引用尚未实际创建的.NET控件。 "句柄被创建"是指操作系统分配给控件一个编号,以允许程序操作其属性。
在这种情况下,通过在窗体的Load事件结束时设置标志,并仅在设置该标志后尝试操纵窗体的控件,可以消除大多数错误。

但是我的文本框已经创建了!在我点击按钮之前,我就可以看到它。如果它没有被创建...那我该如何“创建它”!因为我一点头绪都没有... - OneShot
1
你没有在TextBox上调用Invoke,而是在Form上调用。如果在创建窗体之前或销毁窗体之后调用此方法,就会出现此错误。 - Jeff Yates
我正在尝试向存在于主版本2上的TextBox中添加内容,因为我使用了"Main myClass = new Main()"。我需要找出如何在不必创建新的Main()蓝图的情况下创建myClass。 - OneShot
@OneShot:请查看我给出的另一个答案,可能会有解决方案。 - Jeff Yates

2

这是一个运行时错误,而不是编译器错误。

在调用BeginInvoke或Invoke之前,您的窗体“Main”必须被显示(因此需要创建一个窗口句柄)。

在这种情况下,我通常会让窗体确定是否需要使用调用BeginInvoke或Invoke。您可以通过调用InvokeRequired来测试(请参阅MSDN)。

所以,首先我会在Logging类的updateLog方法中取消logAddDelegate的呼叫。直接调用窗体添加日志即可,如下:

public partial class Main : Form
{
    public Main()
    {
        InitializeComponent();
    }

    private delegate void AddNewLogMessageEventHandler(string message);

    public void AddLogMessage(string message)
    {
        object[] args = new object[1];
        args[0] = message;

        if (InvokeRequired)
            BeginInvoke(new AddNewLogMessageEventHandler(AddLog), args);
        else
            Invoke(new AddNewLogMessageEventHandler(AddLog), args);
    }

    private void AddLog(string message)
    {
        this.Log.Items.Add(message);
    }
 }

所以你可以看到,表单本身负责确定是否需要异步调用该方法。

然而,这仍然无法解决您的运行时错误,因为在显示表单之前您正在调用表单。您可以检查表单的Handle是否为null,这至少允许您验证是否处理有效的表单。


谢谢!是的,我知道这不会解决那个问题...但是一个简单的RichTextBox1(日志)怎么可能无效或未加载表格呢? - OneShot
不是文本框无效,而是窗体无效。在调用窗体之前,必须先创建其句柄。只有当窗体显示时才会创建句柄。尝试使用一些Console.WriteLines,并查看是否可以确定调用何时发生以及以何种顺序发生。 - Chris Holmes
我发现在我的Logging类中,我正在创建一个新的MainClass实例...这就是为什么它还没有被创建的原因。现在我必须想办法,在我的loggingclass中不是创建MainClass的新实例,而是创建同一类的新实例。 - OneShot

1
这是为了帮助其他人遇到同样问题时能够解决。 我的问题: VB.net:“在窗口句柄创建之前,无法在控件上调用Invoke或BeginInvoke。” 我关闭了一个具有处理程序的表单,该处理程序用于更新委托,但没有删除事件的处理程序。
我的做法:当我关闭表单时,我删除了所有处理程序,并在重新打开表单时重新分配它们。这解决了问题。

1

如果您在尚未“显示”的窗口上调用,则会发生该错误。 您确定您的代码中的Logging类(特别是第一行)没有创建主类的第二个实例吗? 可能您调用日志记录的主表单不是您正在查看的主表单。 如果要检查,请在日志记录调用内部添加对“MainClass.Show()”的调用。 如果弹出了第二个副本的主表单,则问题在于您的日志记录类未引用您的表单的正确“实例”。

将类视为“蓝图”。 使用“new”创建的类的每个实例都是另一个对象,从蓝图创建。 仅因为两个对象(在这种情况下,您的两个主要表单)共享相同的蓝图,并不意味着您可以互换使用它们。 在这种情况下,您已经有一个主表单,并且希望“重复使用”它。 您可以尝试:

MainClass myMainForm = Application.OpenForms.OfType<MainClass>().First();
logAddDelegate = myMainForm.logAdd; 
logAddDelegate(message);

在你的日志函数中,使用下面的代码替换当前的代码。不同之处在于,调用Application.OpenForms.OfType().First将进入你的应用程序,并检索出你实际看到的主窗体(技术上,它将检索出第一个实例),并直接在该窗体上进行调用。

希望这可以帮助你。


我的天啊,我觉得这可能行得通,但我还没有让它工作起来。我似乎找不到 Application.OpenForms... 它在哪里呢? - OneShot
System.Windows.Forms.Application.OpenForms - GWLlosa
他说得比我好。 - GWLlosa

1

我发现InvokeRequired不太可靠,所以我简单地使用了

if (!this.IsHandleCreated)
{
    this.CreateHandle();
}

0

logAddDelegate(message);

我认为你是在Form_Load事件被触发之前调用了它。修复你的代码,在调用logAddDelegate(...)即Logging.updateLog()之前等待表单加载。


怎么可能没有加载...在我点击按钮之前,我就一直盯着文本框看。 - OneShot

0

这是你的确切代码吗?在你的add(string)方法中调用了this.Log.Items.Add(message);,但你的日志记录类名为Logging,而不是Log。也许你还有另一个名为Log的窗体吗?如果你在调用add方法时未创建该窗体,将会收到此异常。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接