You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WinForms自定义垂直滚动条内存泄漏问题排查求助

WinForms自定义垂直滚动条内存泄漏问题

问题描述

开发WinForms自定义垂直滚动条控件时,拖动滑块上下移动,应用内存占用持续增长;停止交互后内存也不回落,疑似内存泄漏,但无法定位代码问题。

已尝试操作

  • 合理解绑并重新绑定事件处理程序
  • 在不需要时尝试释放滑块控件
  • 使用Invalidate()方法刷新控件,问题仍存在
  • 手动管理自定义资源(如滑块控件)以防止泄漏

预期行为

交互滚动条时内存占用不应持续增长,停止交互后应恢复正常水平。

代码实现

CustonVScrollBar控件代码

using Chef_s_Codex.CustomUIElements;
using System;
using System.Drawing;
using System.Windows.Forms;

namespace Test.Custom
{
    [DefaultEvent("VelChanged")]
    public partial class CustonVScrollBar : UserControl
    {
        public delegate void VelChangedEventHandler(object sender, EventArgs e);
        public event VelChangedEventHandler VelChanged;

        private Color chennelColor = Color.Transparent;
        private Color slederColor = Color.LightGray;
        private Color focusChennelColor = Color.Transparent;
        private Color focusSlederColor = Color.Gray;
        private int chennelWidth = 15;
        private int slederWidht = 15;
        private int slederHeight = 75;
        private int cornerradius = 7;
        private int maxValue = 100;
        private int minValue = 0;
        private int vel = 0;
        private int scaleValue = 5;

        private int locYSlider = 0;
        private bool isMove = false;
        private int locYLast = 0;
        private bool isFocus = false;
        private int deltaY;
        private RoundedPanel slider;

        #region Properties

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public Color ChennelColor
        {
            get { return chennelColor; }
            set { chennelColor = value; this.BackColor = value; }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public Color SlederColor
        {
            get { return slederColor; }
            set { slederColor = value; if (slider != null) slider.BackColor = value; }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public Color FocusChennelColor
        {
            get { return focusChennelColor; }
            set { focusChennelColor = value; }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public Color FocusSlederColor
        {
            get { return focusSlederColor; }
            set { focusSlederColor = value; }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int ChennelWidth
        {
            get { return chennelWidth; }
            set { chennelWidth = value; Invalidate(); }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int SlederWidht
        {
            get { return slederWidht; }
            set { slederWidht = value; if (slider != null) slider.Width = value; }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int MaxValue
        {
            get { return maxValue; }
            set { maxValue = value; }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int MinValue
        {
            get { return minValue; }
            set { minValue = value; }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int Vel
        {
            get { return vel; }
            set
            {
                if (value > maxValue) vel = maxValue;
                else if (value < minValue) vel = minValue;
                else vel = value;

                locYSlider = UpdateLocYSlider(vel);
            }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int ScaleValue
        {
            get { return scaleValue; }
            set { scaleValue = value; }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int SlederHeight
        {
            get { return slederHeight; }
            set { slederHeight = value; if (slider != null) slider.Height = slederHeight; }
        }

        [Category("Scrollbar")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int CornerRadius
        {
            get { return cornerradius; }
            set { cornerradius = value; }
        }

        #endregion

        public CustonVScrollBar()
        {
            InitializeComponent();

            this.SetStyle(ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
            roundedPanel1.MouseDown -= CustomScrollbar_MouseDown;
            roundedPanel1.MouseUp -= CustomScrollbar_MouseUp;
            roundedPanel1.MouseMove -= CustomScrollbar_MouseMove;
            roundedPanel1.MouseDown += CustomScrollbar_MouseDown;
            roundedPanel1.MouseUp += CustomScrollbar_MouseUp;
            roundedPanel1.MouseMove += CustomScrollbar_MouseMove;

            if (slederHeight < roundedPanel1.Height)
            {
                if (slider != null)
                {
                    roundedPanel1.Controls.Remove(slider);
                    slider.Dispose();
                    slider = null;
                }

                SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true);
                slider = new RoundedPanel();
                roundedPanel1.Controls.Add(slider);
                slider.BorderStyle = BorderStyle.None;
                slider.RadiusBottomLeft = 5;
                slider.RadiusBottomRight = 5;
                slider.RadiusTopLeft = 5;
                slider.RadiusTopRight = 5;
                slider.Height = slederHeight;
                slider.Width = slederWidht;
                slider.BackColor = slederColor;
                slider.Location = new Point((roundedPanel1.Width - slederWidht) / 2, 20);
                slider.Enabled = false;
            }

            locYSlider = 1;
            locYLast = 1;
            vel = UpdateValue();
        }

        protected override void CreateHandle()
        {
            base.CreateHandle();
            roundedPanel1.BackColor = chennelColor;
            this.BackColor = this.Parent.BackColor;
        }

        #region Callbacks

        private void CustomScrollbar_MouseMove(object? sender, MouseEventArgs e)
        {
            if (slederHeight >= roundedPanel1.Height || !isMove) return;

            int allowedTop = 1;
            int allowedBottom = roundedPanel1.Height - 1;

            deltaY = e.Y - locYLast;
            if (deltaY == 0) return;

            int newLocYSlider = locYSlider + deltaY;
            newLocYSlider = Math.Clamp(newLocYSlider, allowedTop, allowedBottom - slederHeight);

            if (newLocYSlider == locYSlider) return; // Prevent unnecessary updates

            locYSlider = newLocYSlider;
            locYLast = e.Y;

            vel = UpdateValue();
            OnVelChanged();
        }

        protected virtual void OnVelChanged()
        {
            VelChanged?.Invoke(this, EventArgs.Empty);
        }

        private int UpdateValue()
        {
            int availableHeight = this.Height - slederHeight;

            if (availableHeight <= 0) return minValue; // Prevent division by zero

            double ratio = (double)(locYSlider) / availableHeight;

            int value = minValue + (int)((maxValue - minValue) * ratio);

            slider.Location = new Point(slider.Location.X, locYSlider);

            return Math.Clamp(value, minValue, maxValue);
        }

        private void CustomScrollbar_MouseUp(object? sender, MouseEventArgs e)
        {
            slider.BackColor = slederColor;

            if (slederHeight >= roundedPanel1.Height) return;

            isMove = false;
            isFocus = false;
        }

        private void CustomScrollbar_MouseDown(object? sender, MouseEventArgs e)
        {
            slider.BackColor = focusSlederColor;

            if (slederHeight >= roundedPanel1.Height) return;

            isFocus = true;
            locYLast = e.Y;

            if (e.Button == MouseButtons.Left)
            {
                locYSlider = e.Y - (slederHeight / 2);

                if (locYSlider < 0) locYSlider = 0;
                if (locYSlider > roundedPanel1.Height - slederHeight) locYSlider = roundedPanel1.Height - slederHeight;

                vel = UpdateValue();
                OnVelChanged();

                isMove = true;
            }
        }

        private int UpdateLocYSlider(int valueSlider)
        {
            double d = (double)roundedPanel1.Height / (double)(maxValue - minValue);
            int y = (int)(valueSlider * d);
            if (y > roundedPanel1.Height - slederHeight) y -= slederHeight;
            return y;
        }

        #endregion
    }
}

RoundedPanel控件代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Test.Custom
{
    public class RoundedPanel : Panel
    {
        private int radiusTopLeft = 0;
        private int radiusTopRight = 0;
        private int radiusBottomLeft = 0;
        private int radiusBottomRight = 0;

        public bool draw;
        public bool isTest;

        Graphics graphics;

        [Browsable(true)]
        [Category("Corner Radius")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int RadiusTopLeft
        {
            get => radiusTopLeft;
            set { radiusTopLeft = Math.Max(0, value); Invalidate(); }
        }

        [Browsable(true)]
        [Category("Corner Radius")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int RadiusTopRight
        {
            get => radiusTopRight;
            set { radiusTopRight = Math.Max(0, value); Invalidate(); }
        }

        [Browsable(true)]
        [Category("Corner Radius")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int RadiusBottomLeft
        {
            get => radiusBottomLeft;
            set { radiusBottomLeft = Math.Max(0, value); Invalidate(); }
        }

        [Browsable(true)]
        [Category("Corner Radius")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int RadiusBottomRight
        {
            get => radiusBottomRight;
            set { radiusBottomRight = Math.Max(0, value); Invalidate(); }
        }

        public RoundedPanel()
        {
            this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true);
            this.BorderStyle = BorderStyle.None;
            graphics = this.CreateGraphics();
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            graphics.CompositingQuality = CompositingQuality.HighQuality;
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
        }

        private GraphicsPath GetFigurePath(RectangleF rect)
        {
            GraphicsPath path = new GraphicsPath();
            path.StartFigure();

            // Top-left corner
            if (RadiusTopLeft > 0)
                path.AddArc(rect.X, rect.Y, RadiusTopLeft * 2, RadiusTopLeft * 2, 180, 90);
            else
                path.AddLine(rect.X, rect.Y, rect.X + 1, rect.Y);

            // Top-right corner
            if (RadiusTopRight > 0)
                path.AddArc(rect.Width - RadiusTopRight * 2, rect.Y, RadiusTopRight * 2, RadiusTopRight * 2, 270, 90);
            else
                path.AddLine(rect.Width - 1, rect.Y, rect.Width, rect.Y + 1);

            // Bottom-right corner
            if (RadiusBottomRight > 0)
                path.AddArc(rect.Width - RadiusBottomRight * 2, rect.Height - RadiusBottomRight * 2, RadiusBottomRight * 2, RadiusBottomRight * 2, 0, 90);
            else
                path.AddLine(rect.Width, rect.Height - 1, rect.Width - 1, rect.Height);

            // Bottom-left corner
            if (RadiusBottomLeft > 0)
                path.AddArc(rect.X, rect.Height - RadiusBottomLeft * 2, RadiusBottomLeft * 2, RadiusBottomLeft * 2, 90, 90);
            else
                path.AddLine(rect.X + 1, rect.Height, rect.X, rect.Height - 1);

            path.CloseFigure();
            return path;
        }

        protected override void OnResize(EventArgs eventargs)
        {
            base.OnResize(eventargs);

            RectangleF rectangleF = new RectangleF(0, 0, this.Width, this.Height + 1);

            if (this.Region != null)
            {
                this.Region.Dispose();
            }

            using (GraphicsPath path = GetFigurePath(rectangleF))
            using (Pen pen = new Pen(this.Parent != null ? this.Parent.BackColor : this.BackColor, 2))
            {

                this.Region = new Region(path);
                graphics.DrawPath(pen, path);

            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.Region != null)
                {
                    this.Region.Dispose();
                    this.Region = null;
                }
            }
            base.Dispose(disposing);
        }
    }
}

问题分析与修复方案

内存泄漏根源

  1. RoundedPanel中graphics对象未释放:构造函数中直接创建Graphics实例,但未在Dispose方法中释放,每次创建RoundedPanel都会导致该对象无法被回收。
  2. OnResize方法中图形资源重复创建且管理不当:拖动滑块时会间接触发RoundedPanel的Resize事件,频繁创建Region和GraphicsPath,部分资源未被及时回收。
  3. 事件绑定冗余:CustonVScrollBar构造函数中先解绑再绑定事件,虽不直接泄漏,但可能引发潜在的事件引用问题。

修复步骤

1. 修复RoundedPanel的Graphics资源泄漏

在Dispose方法中添加graphics释放逻辑,同时移除构造函数中预创建Graphics的代码,将绘制逻辑移至OnPaint方法:

public RoundedPanel()
{
    this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true);
    this.BorderStyle = BorderStyle.None;
}

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
    e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
    e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

    RectangleF rectangleF = new RectangleF(0, 0, this.Width, this.Height + 1);
    using (GraphicsPath path = GetFigurePath(rectangleF))
    using (Pen pen = new Pen(this.Parent != null ? this.Parent.BackColor : this.BackColor, 2))
    {
        this.Region = new Region(path);
        e.Graphics.DrawPath(pen, path);
    }
}

protected override void OnResize(EventArgs eventargs)
{
    base.OnResize(eventargs);
    Invalidate(); // 触发OnPaint重绘,避免在Resize中直接操作图形资源
}

protected override void Dispose(bool disposing)
{
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 23:27:09