如何:将工作安排在指定的同步上下文上
此示例演示如何在 Windows Presentation Foundation (WPF) 应用程序中使用 TaskScheduler.FromCurrentSynchronizationContext 方法,将任务安排在创建用户界面 (UI) 控件所基于的同一线程上。
过程
创建 WPF 项目
在 Visual Studio 中,创建一个 WPF 应用程序项目并为其命名。
在设计视图中,将 Image 控件从**“工具箱”**拖到设计图面。 在 XAML 视图中,将水平对齐方式指定为“Left”。由于将在运行时动态调整控件大小,因此大小无关紧要。 接受默认名称“image1”。
将一个按钮从**“工具箱”**拖到应用程序窗口的左下角。 双击该按钮,以添加 Click 事件处理程序。 在 XAML 视图中,将按钮的 Content 属性设置为“Make a Mosaic”,并将其水平对齐方式指定为“Left”。
在 MainWindow.xaml.cs 文件中,使用以下代码替换该文件的整个内容。 确保命名空间中的名称与项目名称匹配。
按 F5 运行该应用程序。 每次单击按钮时,应会显示图块的一种新排列方式。
示例
说明
下面的示例将创建一个由多个图像组成的镶嵌画,这些图像是从指定目录随机选择的。 WPF 对象用于加载图像和调整图像的大小。 然后,将原始像素传递到一个任务,该任务使用 ParallelFor() 循环将像素数据写入一个很大的单字节数组。 由于没有两个图块占用相同的数组元素,因此无需同步。 也可以按任意顺序写入图块,因为它们的位置是独立于任何其他图块计算而得的。 然后,会将该大数组传递到在 UI 线程上运行的任务,像素数据将在该线程中加载到 Image 控件内。
代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace wpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private int fileCount;
int colCount;
int rowCount;
private int tilePixelHeight;
private int tilePixelWidth;
private int largeImagePixelHeight;
private int largeImagePixelWidth;
private int largeImageStride;
PixelFormat format;
BitmapPalette palette;
public MainWindow()
{
InitializeComponent();
// For this example, values are hard-coded to a mosaic of 8x8 tiles.
// Each tile is 50 pixels high and 66 pixels wide and 32 bits per pixel.
colCount = 12;
rowCount = 8;
tilePixelHeight = 50;
tilePixelWidth = 66;
largeImagePixelHeight = tilePixelHeight * rowCount;
largeImagePixelWidth = tilePixelWidth * colCount;
largeImageStride = largeImagePixelWidth * (32 / 8);
this.Width = largeImagePixelWidth + 40;
image1.Width = largeImagePixelWidth;
image1.Height = largeImagePixelHeight;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
// For best results use 1024 x 768 jpg files at 32bpp.
string[] files = System.IO.Directory.GetFiles(@"C:\Users\Public\Pictures\Sample Pictures\", "*.jpg");
fileCount = files.Length;
Task<byte[]>[] images = new Task<byte[]>[fileCount];
for (int i = 0; i < fileCount; i++)
{
int x = i;
images[x] = Task.Factory.StartNew(() => LoadImage(files[x]));
}
// When they�ve all been loaded, tile them into a single byte array.
var tiledImage = Task.Factory.ContinueWhenAll(
images, (i) => TileImages(i));
// We are currently on the UI thread. Save the sync context and pass it to
// the next task so that it can access the UI control "image1".
var UISyncContext = TaskScheduler.FromCurrentSynchronizationContext();
// On the UI thread, put the bytes into a bitmap and
// and display it in the Image control.
var t3 = tiledImage.ContinueWith((antedecent) =>
{
// Get System DPI.
Matrix m = PresentationSource.FromVisual(Application.Current.MainWindow)
.CompositionTarget.TransformToDevice;
double dpiX = m.M11;
double dpiY = m.M22;
BitmapSource bms = BitmapSource.Create( largeImagePixelWidth,
largeImagePixelHeight,
dpiX,
dpiY,
format,
palette, //use default palette
antedecent.Result,
largeImageStride);
image1.Source = bms;
}, UISyncContext);
}
byte[] LoadImage(string filename)
{
// Use the WPF BitmapImage class to load and
// resize the bitmap. NOTE: Only 32bpp formats are supported correctly.
// Support for additional color formats is left as an exercise
// for the reader. For more information, see documentation for ColorConvertedBitmap.
BitmapImage myBitmapImage = new BitmapImage();
myBitmapImage.BeginInit();
myBitmapImage.UriSource = new Uri(filename);
tilePixelHeight = myBitmapImage.DecodePixelHeight = tilePixelHeight;
tilePixelWidth = myBitmapImage.DecodePixelWidth = tilePixelWidth;
myBitmapImage.EndInit();
format = myBitmapImage.Format;
int size = (int)(myBitmapImage.Height * myBitmapImage.Width);
int stride = (int)myBitmapImage.Width * 4;
byte[] dest = new byte[stride * tilePixelHeight];
myBitmapImage.CopyPixels(dest, stride, 0);
return dest;
}
int Stride(int pixelWidth, int bitsPerPixel)
{
return (((pixelWidth * bitsPerPixel + 31) / 32) * 4);
}
// Map the individual image tiles to the large image
// in parallel. Any kind of raw image manipulation can be
// done here because we are not attempting to access any
// WPF controls from multiple threads.
byte[] TileImages(Task<byte[]>[] sourceImages)
{
byte[] largeImage = new byte[largeImagePixelHeight * largeImageStride];
int tileImageStride = tilePixelWidth * 4; // hard coded to 32bpp
Random rand = new Random();
Parallel.For(0, rowCount * colCount, (i) =>
{
// Pick one of the images at random for this tile.
int cur = rand.Next(0, sourceImages.Length);
byte[] pixels = sourceImages[cur].Result;
// Get the starting index for this tile.
int row = i / colCount;
int col = (int)(i % colCount);
int idx = ((row * (largeImageStride * tilePixelHeight)) + (col * tileImageStride));
// Write the pixels for the current tile. The pixels are not contiguous
// in the array, therefore we have to advance the index by the image stride
// (minus the stride of the tile) for each scanline of the tile.
int tileImageIndex = 0;
for (int j = 0; j < tilePixelHeight; j++)
{
// Write the next scanline for this tile.
for (int k = 0; k < tileImageStride; k++)
{
largeImage[idx++] = pixels[tileImageIndex++];
}
// Advance to the beginning of the next scanline.
idx += largeImageStride - tileImageStride;
}
});
return largeImage;
}
}
}
注释
此示例演示如何将数据移出 UI 线程,通过使用并行循环和 Task 对象修改数据,然后将数据传递回在 UI 线程上运行的任务。 当您必须使用任务并行库来执行 WPF API 不支持或速度不够快的操作时,此方法非常有用。 在 WPF 中创建图像镶嵌画的另一种方法是使用 WrapPanel 对象,并将图像添加到该对象中。 WrapPanel 将处理定位图块的工作。 但是,此工作只能在 UI 线程上执行。
此示例有一些限制。 例如,只支持每像素 32 位的图像;其他格式的图像在大小调整过程中会被 BitmapImage 对象损坏。 此外,源图像必须都比图块大小大。 作为进一步的练习,您可以添加处理多种像素格式和文件大小的功能。