在使用 Graphics.DrawString() 时出现了访问冲突错误。

3

我有一个Windows窗体,上面有一些数字(当前只有一个数字),并定期刷新它们(数字是随机生成的)。更新过程在应用程序的另一个线程中进行,以便窗体可以接收到用户点击按钮、调整窗口大小等事件。

主方法:

class Program {       
        static void Main(string[] args) {
            Form o = new Overlay();
            Application.Run(o);
        }
    }

表单类:

public partial class Overlay : Form {
        Graphics g;
        Drawer drawer;
        public Overlay() {
            InitializeComponent();
            TopMost = true;
            TransparencyKey = Color.Black;
            BackColor = TransparencyKey;
            CheckForIllegalCrossThreadCalls = false;

            g = CreateGraphics();
            drawer = new Drawer();
        }

        protected override void OnLoad(EventArgs e) {
            base.OnLoad(e);
            Thread upd = new Thread(mainLoop);
            upd.Start();
        }

        void mainLoop() {
            while (true) {
                NetCoreEx.Geometry.Rectangle r;
                GetWindowRect(Handle, out r);
                Refresh();
                drawer.Update(g, new Rectangle(r.Left, r.Top, r.Width, r.Height));
            }
        }

抽屉:

class Drawer {
        Font font;

        public Drawer() {
            string workingDir = Environment.CurrentDirectory;
            string projDir = Directory.GetParent(workingDir).Parent.FullName;
            PrivateFontCollection collection = new PrivateFontCollection();
            collection.AddFontFile(projDir + "\\Resources\\Athelas-Regular.ttf");
            FontFamily fontFamily = new FontFamily("Athelas", collection);
            font = new Font(fontFamily, 16);
        }

        
        public void Update(Graphics g, Rectangle rect) {
            string stats = new Random().NextDouble().ToString();
            g.DrawString(stats, font, Brushes.Aqua, rect.Width / 2, (int)(rect.Height * 0.75));
        }
    }

这段代码看起来很简单,但是由于某些原因,在正常运行5-10秒后应用程序突然崩溃,在DrawString方法上抛出System.AccessViolationException异常...堆栈跟踪:

at System.Drawing.SafeNativeMethods.Gdip.GdipDrawString(HandleRef graphics, String textString, Int32 length, HandleRef font, GPRECTF& layoutRect, HandleRef stringFormat, HandleRef brush)
   at System.Drawing.Graphics.DrawString(String s, Font font, Brush brush, RectangleF layoutRectangle, StringFormat format)
   at System.Drawing.Graphics.DrawString(String s, Font font, Brush brush, Single x, Single y)
   at OverlayStatistics.Scripts.Drawer.Update(Graphics g, Rectangle rect) in C:\Users\Гриша\source\repos\OverlayStatistics\Scripts\Drawer.cs:line 30
   at OverlayStatistics.Overlay.mainLoop() in C:\Users\Гриша\source\repos\OverlayStatistics\Scripts\Overlay.cs:line 50
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

我已经花了很多时间进行调试,但仍然不知道我做错了什么,请帮忙吗?


1
Winforms图形基本规则#1:永远不要使用control.CreateGraphics!永远不要尝试缓存Graphics对象!您可以使用Graphics g = Graphics.FromImage(bmp)将其绘制到Bitmap bmp中,或者在控件的“Paint”事件中使用e.Graphics参数。 - TaW
2
规则#2:Winforms 不是线程安全的。使用 Invoke()不要禁用 CheckForIllegalCrossThreadCalls,它存在有其原因。 - Charlieface
@TaW 像这样吗?protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Rectangle r = e.ClipRectangle; drawer.Update(e.Graphics, new Rectangle(r.Left, r.Top, r.Width, r.Height)); }在计时器/线程循环中使用 Refresh() 仍然会出现相同的错误。 - Fizz Areh
2
其他问题:#3 对资源使用“using”块或“IDisposable” #4 不要在没有计时器或waithandle的情况下无限循环线程,除非您想占用CPU #5 “new Random().NextDouble()”通常会得到相同的结果。缓存Random。 - Charlieface
在计时器Tick事件中使用Invalidate。如果仍然崩溃,则存在更多错误。 - TaW
谢谢大家的回复,这些事情肯定应该做好。 - Fizz Areh
1个回答

4
发生的情况是FontFamily实例被处理,导致内部GDI调用崩溃。您可以通过以下方式加速此行为:
public void Update(Graphics g, Rectangle rect)
{
    string stats = new Random().NextDouble().ToString();
    g.DrawString(stats, font, Brushes.Aqua, rect.Width / 2, (int)(rect.Height * 0.75));
    GC.Collect(); // a good way to check for dispose issues
}

有多种方法可以解决它,例如只需要确保FontFamily实例也是Drawer的成员即可:

class Drawer {
    Font font;
    FontFamily fontFamily;


    public Drawer() {
        ...
        fontFamily = new FontFamily("Athelas", collection);
        font = new Font(fontFamily, 16);
    }

    public void Update(Graphics g, Rectangle rect) {
        string stats = new Random().NextDouble().ToString();
        g.DrawString(stats, font, Brushes.Aqua, rect.Width / 2, (int)(rect.Height * 0.75));
    }
}

哇,看起来这是一个真正的解决方案(至少现在已经连续运行了3分钟)。 - Fizz Areh
@FizzAreh - 没有什么魔法。如果你想确保,而不是等待,可以添加GC.Collect。 - Simon Mourier

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