如何:将工作安排在指定的同步上下文上

此示例演示如何在 Windows Presentation Foundation (WPF) 应用程序中使用 TaskScheduler.FromCurrentSynchronizationContext 方法,将任务安排在创建用户界面 (UI) 控件所基于的同一线程上。

过程

创建 WPF 项目

  1. 在 Visual Studio 中,创建一个 WPF 应用程序项目并为其命名。

  2. 在设计视图中,将 Image 控件从**“工具箱”**拖到设计图面。 在 XAML 视图中,将水平对齐方式指定为“Left”。由于将在运行时动态调整控件大小,因此大小无关紧要。 接受默认名称“image1”。

  3. 将一个按钮从**“工具箱”**拖到应用程序窗口的左下角。 双击该按钮,以添加 Click 事件处理程序。 在 XAML 视图中,将按钮的 Content 属性设置为“Make a Mosaic”,并将其水平对齐方式指定为“Left”。

  4. 在 MainWindow.xaml.cs 文件中,使用以下代码替换该文件的整个内容。 确保命名空间中的名称与项目名称匹配。

  5. 按 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 对象损坏。 此外,源图像必须都比图块大小大。 作为进一步的练习,您可以添加处理多种像素格式和文件大小的功能。

请参见

概念

任务计划程序

Advanced Topics (Task Parallel Library)