I have a Windows Forms application that copies and re-sizes images references in a GEDCOM genealogy file. The user selects the file and output directory as well as the re-sizing options from the main form which then opens another form as a dialog which contains a label, progress bar, and button. I’m updating the application to use the new asynchronous features in .NET 4.5 and also modifying it to make use of parallel processing. Everything works fine, except I’m noticing that the UI responsiveness is a bit choppy (stutters); If I don’t update the message label with the percent, then it’s much smoother. Also, when I cancel the task, the UI will hang for anywhere from 1 to 15 seconds. The application is just for my personal use, so it’s not that big of a deal, but I’m curious what might be causing the issue and what the recommended way to deal with it is. Is the parallel processing just overloading the CPU with having too many threads to process? I tried adding a Thread.Sleep(100) to each loop iteration and it seemed to help a little bit.
Here is a minimal version of the application that still causes the issues. To reproduce:
- Create a new windows forms application with the below form.
- Create a directory with a bunch of jpeg images (50+ images)
- Replace the _SourceDirectoryPath and _DestinationDirectoryPath variables with your directories.
- Run application
Designer.cs:
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.lblMessage = new System.Windows.Forms.Label();
this.pgProgressBar = new System.Windows.Forms.ProgressBar();
this.btnStart = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// lblMessage
//
this.lblMessage.AutoSize = true;
this.lblMessage.Location = new System.Drawing.Point(32, 25);
this.lblMessage.Name = "lblMessage";
this.lblMessage.Size = new System.Drawing.Size(0, 13);
this.lblMessage.TabIndex = 0;
//
// pgProgressBar
//
this.pgProgressBar.Location = new System.Drawing.Point(35, 51);
this.pgProgressBar.Name = "pgProgressBar";
this.pgProgressBar.Size = new System.Drawing.Size(253, 23);
this.pgProgressBar.TabIndex = 1;
//
// btnStart
//
this.btnStart.Location = new System.Drawing.Point(132, 97);
this.btnStart.Name = "btnStart";
this.btnStart.Size = new System.Drawing.Size(75, 23);
this.btnStart.TabIndex = 2;
this.btnStart.Text = "Start";
this.btnStart.UseVisualStyleBackColor = true;
this.btnStart.Click += new System.EventHandler(this.btnStart_Click);
//
// btnCancel
//
this.btnCancel.Location = new System.Drawing.Point(213, 97);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(75, 23);
this.btnCancel.TabIndex = 3;
this.btnCancel.Text = "Cancel";
this.btnCancel.UseVisualStyleBackColor = true;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(315, 149);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.btnStart);
this.Controls.Add(this.pgProgressBar);
this.Controls.Add(this.lblMessage);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label lblMessage;
private System.Windows.Forms.ProgressBar pgProgressBar;
private System.Windows.Forms.Button btnStart;
private System.Windows.Forms.Button btnCancel;
}
Code:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
public partial class Form1 : Form
{
private CancellationTokenSource _CancelSource;
private string _SourceDirectoryPath = @"Your\Source\Directory";
private string _DestinationDirectoryPath = @"Your\Destination\Directory";
public Form1()
{
InitializeComponent();
lblMessage.Text = "Click Start to begin extracting images";
btnCancel.Enabled = false;
_CancelSource = new CancellationTokenSource();
}
private async void btnStart_Click(object sender, EventArgs e)
{
btnStart.Enabled = false;
btnCancel.Enabled = true;
List<string> files = await Task.Run(() => Directory.GetFiles(_SourceDirectoryPath, "*.jpg").ToList());
// scan/extract files
Progress<int> progress = new Progress<int>(UpdateProgress);
int result = await Task.Run(() => ExtractFiles(files, progress, _CancelSource.Token));
if (_CancelSource.IsCancellationRequested)
{
lblMessage.Text = "Extraction cancelled by user.";
}
else
{
lblMessage.Text = string.Format("Extraction Complete: {0} files extracted.", result);
}
btnStart.Enabled = true;
btnCancel.Enabled = false;
}
private void btnCancel_Click(object sender, EventArgs e)
{
lblMessage.Text = "Cancelling...";
btnCancel.Enabled = false;
_CancelSource.Cancel();
}
private void UpdateProgress(int value)
{
lblMessage.Text = string.Format("Extracting files: {0}%", value);
pgProgressBar.Value = value;
}
public int ExtractFiles(List<string> fileReferences, IProgress<int> progress, CancellationToken cancelToken)
{
double totalFiles = fileReferences.Count;
int processedCount = 0;
int extractedCount = 0;
int previousPercent = 0;
Directory.CreateDirectory(_DestinationDirectoryPath);
Parallel.ForEach(fileReferences, (reference, state) =>
{
if (cancelToken.IsCancellationRequested)
{
state.Break();
}
string fileName = Path.GetFileName(reference);
string filePath = Path.Combine(_DestinationDirectoryPath, fileName);
using (Image image = Image.FromFile(reference))
{
using (Image newImage = ResizeImage(image, 1000, 1000))
{
newImage.Save(filePath);
Interlocked.Increment(ref extractedCount);
}
}
Interlocked.Increment(ref processedCount);
int percent = (int)(processedCount / totalFiles * 100);
if (percent > previousPercent)
{
progress.Report(percent);
Interlocked.Exchange(ref previousPercent, percent);
}
});
return extractedCount;
}
public Image ResizeImage(Image image, int maxWidth, int maxHeight)
{
Image newImage = null;
if (image.Width > maxWidth || image.Height > maxHeight)
{
double widthRatio = (double)maxWidth / (double)image.Width;
double heightRatio = (double)maxHeight / (double)image.Height;
double ratio = Math.Min(widthRatio, heightRatio);
int newWidth = (int)(image.Width * ratio);
int newHeight = (int)(image.Height * ratio);
newImage = new Bitmap(newWidth, newHeight);
using (Graphics graphic = Graphics.FromImage(newImage))
{
graphic.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
graphic.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphic.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphic.DrawImage(image, 0, 0, newWidth, newHeight);
}
}
return newImage;
}
}