您希望通过 Windows 窗体应用程序实现的效果相当简单。有很多选项可以让您到达那里,但我将介绍 Paint 事件选项,我们可以在其中绘制自定义矩形以向用户显示进度。有两种方法可以有效地展示这条路线;其中一种是使用Timer 的简单方法,另一种是更深入的方法,但更适合显示后台线程的进度。
Timer 选项
使用Timer 控件,我们可以用最少的代码重现这种效果。您只需要 Panel、Timer 和 float 即可跟踪进度。在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 必须是介于0 和1 之间的float 值;否则在执行上述操作时,您必须除以100。如果进度在0 和1 之间,只需乘以100 即可显示。我总是发现在0 和1 之间保持进度更容易,因为我经常使用它进行计算,并且只使用一次显示。
以 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 线程的return。 Send 最适合当 GUI 需要处理后台线程发送的数据,然后将其发回或更新后台线程上的某些内容时。
祝你在未来的努力中好运!