【问题标题】:Filling a panel as progress bar填充面板作为进度条
【发布时间】:2018-11-28 15:43:59
【问题描述】:

我有一个关于 winform itch 的面板,带有一个按钮、2 个标签和一个图像。现在我想将面板背景的颜色从下到上逐渐更改为进度条。我尝试使用带有组件的面板,第二个面板在第一个面板的顶部但在组件的后面,然后逐渐扩大第二个面板的高度。但是组件的背景颜色提醒了第一个面板的颜色。我尝试将组件的背景颜色设置为透明。

有没有人建议实现这种效果?它不一定是面板,我想要的只是组件保持在该区域的顶部并且背景颜色发生变化。

提前致谢

【问题讨论】:

  • 您始终可以使用OnPaint 事件自定义从下到上随着时间的推移绘制一个矩形(也许触发某种计时器以逐步提高绘制事件或完全执行绘制)。我相信您可以使用panel1.Invalidate() 强制控件重新绘制自身,从而引发OnPaint 事件让您重新绘制进度。现在写一个答案。

标签: c# winforms progress-bar


【解决方案1】:

您希望通过 Windows 窗体应用程序实现的效果相当简单。有很多选项可以让您到达那里,但我将介绍 Paint 事件选项,我们可以在其中绘制自定义矩形以向用户显示进度。有两种方法可以有效地展示这条路线;其中一种是使用Timer 的简单方法,另一种是更深入的方法,但更适合显示后台线程的进度。


Timer 选项

使用Timer 控件,我们可以用最少的代码重现这种效果。您只需要 PanelTimerfloat 即可跟踪进度。在FormLoad 我们启动计时器,在TimerTick 我们增加进度并使面板无效,在PanelPaint 我们绘制我们的自定义进度:

private float progress = 0f;
private void Form1_Load(object sender, EventArgs e) => timer1.Start();
private void timer1_Tick(object sender, EventArgs e) {
    progress += 0.01f;
    if (progress >= 1.0f)
        progress = 0f;

    panel1.Invalidate();
}
private void panel1_Paint(object sender, PaintEventArgs e) {
    e.Graphics.FillRectangle(Brushes.Green, new Rectangle(0, 0, panel1.Width, (int)(panel1.Height * progress)));
}

如您所见,此方法的代码非常简单且易于解释。


事件选项

在后台线程上加载内容时,更新 GUI 元素(例如 Panel)时,事情会变得更加复杂。在这种情况下,我更喜欢使用SynchronizationContext 来引发表单上的事件,这些事件将更新GUI 元素而不引发CrossThreadExceptions。然而,在这个特定的示例中,事情相对简单,工作方式与上述相同,但涉及更多代码。假设我们有一个class 来处理我们所有的后台加载,这一切都发生在一个单独的线程上;在这种情况下,我们有一个表单订阅的自定义事件,SyncrhonizationContext 引发该事件,因此表单可以更新 GUI。

public class DataLoader {

    #region Fields

    private bool loading = true;
    private Thread loadingThread;
    private SynchronizationContext loadingContext;

    #endregion

    #region Properties

    public float Progress { get; private set; } = 0f;

    #endregion

    #region Events

    public event EventHandler ObjectLoaded;
    private void OnObjectLoaded() => loadingContext.Post(new SendOrPostCallback(PostObjectLoaded), new EventArgs());
    private void PostObjectLoaded(object data) => ObjectLoaded?.Invoke(this, (EventArgs)data);

    #endregion

    #region Constructor(s)

    public DataLoader() {
        if (SynchronizationContext.Current != null)
            loadingContext = SynchronizationContext.Current;
        else
            loadingContext = new SynchronizationContext();

        loadingThread = new Thread(new ThreadStart(LoadData));
        loadingThread.IsBackground = true;
        loadingThread.Start();
    }

    #endregion


    #region Private Methods

    private void LoadData() {
        while (loading) {
            // Do some cool stuff to load data.
            CoolStuff();

            // Increment progress.
            Progress += 0.01f;
            if (Progress >= 1.0f)
                loading = false;

            // Now this object is loaded, raise event for subscribers.
            OnObjectLoaded();
        }
    }
    private void CoolStuff() {
        // Do cool loading stuff.
    }

    #endregion

}

现在我们的加载类已经构建并加载了对象和所有有趣的爵士乐,我们可以将它添加到我们的表单代码中,以执行计时器正在执行的相同操作。这样我们订阅ObjectLoaded 事件,并遵循相同的流程。

private DataLoader loader;
private void Form1_Load(object sender, EventArgs e) {
    loader = new DataLoader();
    loader.ObjectLoaded += loader_ObjectLoaded;
}
private void loader_ObjectLoaded(object sender, EventArgs e) => panel1.Invalidate();
private void panel1_Paint(object sender, PaintEventArgs e) {
    e.Graphics.FillRectangle(Brushes.Green, new Rectangle(0, 0, panel1.Width, (int)(panel1.Height * loader.Progress)));
}

从底部填充一个矩形

要从底部填充矩形,您必须同时调整位置和高度。

int height = panel1.Height * progress;
Rectangle bounds = new Rectangle(0, panel1.height - height, panel1.Width, height);
e.Graphics.FillRectangle(Brushes.Green, bounds)

您可以按照从右到左的相同概念和从左到右的原始示例。

width = panel1.width * progress;
Rectangle bounds = new Rectangle(panel1.Width - width, 0, width, panel1.Height);

还有一些技术可以从特定点开始以实现一些烟雾和镜子类型的外观(例如形状奇特的装载机),但我会将这些留给您发现是否需要它们。

还请记住,您的Progress 必须是介于01 之间的float 值;否则在执行上述操作时,您必须除以100。如果进度在01 之间,只需乘以100 即可显示。我总是发现在01 之间保持进度更容易,因为我经常使用它进行计算,并且只使用一次显示。


以 100% 停止进度

根据您的评论,我相信您在我的示例中留下了进度增量,并且增量为0.01f。这不是执行此操作的正确方法,仅用于示例。

传统上,您需要计算出您的任务总数(或者对于文件的总大小),然后将完成的数量除以该总数来获得您的进度。下面是一个基本示例,其中包含要处理的对象列表。

private List<object> ObjectsToLoad = new List<object>();
private void DoCoolStuff() {
    int objectsLoaded = 0;
    foreach (object o in ObjectsToLoad) {
        // Process the object.
        Progress = (float)++objectsLoaded / (float)ObjectsToLoad.Count;;
        OnObjectLoaded();
    }
}

在这种特殊情况下,您将在 Load 方法的 while 循环中删除对 OnObjectLoaded 的调用,以防止重复事件引发。


如果您对所使用的任何类型不熟悉,请随时查看 MSDN 上的文档;我在下面列出了最不常见的,如果您感兴趣的没有列出,我深表歉意。您可以随时评论,我会添加它。

另外,回答未来读者可能提出的问题;我使用loadingContext.Post 而不是loadingContext.Send 的原因是因为在这种情况下,我不相信后台线程真的关心GUI 需要做什么,它只需要让GUI 知道去做。 Post 告诉线程继续处理,而Send 告诉线程等待来自 GUI 线程的returnSend 最适合当 GUI 需要处理后台线程发送的数据,然后将其发回或更新后台线程上的某些内容时。

祝你在未来的努力中好运!

【讨论】:

  • 非常感谢。这就是我要找的。我有测试应用程序与展位示例一起运行 :-) 几个问题:也许是一个愚蠢的问题,但我怎样才能从底部填充面板?我不确定您对十六进制值和进度的增量究竟做了什么。在第二个例子中,当“做酷的东西”完成时,如何确保面板停止填充 100%?假设复制一些大文件很酷?
  • @AndersPedersen 要从底部填充面板,您必须调整矩形的位置和高度;我会用一个例子来更新我的帖子。不确定十六进制值是什么意思。就 100% 停止进度而言,您必须按照已完成的任务来划分任务;我也会更新一个样本。如果你正在加载一个文件,通常有一种方法可以告诉你已经处理了多少,但具体的实现会根据你加载它的方式而有所不同。给我一点时间来发布样本。完成后我会发表评论。
  • @AndersPedersen 我更新了我的帖子,如果有任何问题或不起作用,请告诉我,我会在明天早上修复它。我现在正在打电话,因为我现在没有电脑。另外,如果您澄清有关十六进制值的问题,我也可以尝试回答。如果帖子解决了您的问题,请记住将其标记为已回答。 :)
  • 再次感谢您的大力支持。我让它使用 List 示例从底部填充它。唯一缺少的是在 ForEach 之后 - loading = false;停止 while 循环。
  • @AndersPedersen 我还是不太清楚你所说的十六进制是什么意思;如果你在谈论 0.01f 那么这是一个浮点值,不要害怕 f 欺骗你。就同​​步上下文而言,它有点像帮助后台线程与 GUI 线程通信的信使。最接近异步等待的是我在上面介绍的发送与帖子。如果您的后台线程与您的 GUI 线程异步运行并且您使用 Send,那么它类似于使用 Post 时的等待,后台线程发送消息并且不会等待。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多