WPF RichTextBox 插入图片和撤消操作

WPF RichTextBox Insert Image and Undo Operation

我有将图像从剪贴板导入 RichtTextBox 的代码。

Image i = new Image();
i.Source =  Clipboard.GetImage();
paragraph.Inlines.Add(i);

当我尝试删除图像并按 Undo() 时出现异常。

No matching constructor found on type 'System.Windows.Interop.InteropBitmap'. You can use the Arguments or FactoryMethod directives to construct this type.' Line number '1' and line position '226'.

这是因为 RichTextBox 生成的 XAML 如下所示:

<Image.Source><swi:InteropBitmap /></Image.Source>

我尝试将 BitmapSource 的类型更改为 BitmapImage。但在这种情况下,我有 XAML:

<Image.Source><BitmapImage BaseUri="{x:Null}" /></Image.Source></Image>

删除后,撤消我有例外:

Exception thrown: 'System.Windows.Markup.XamlParseException' in PresentationFramework.dll

Additional information: 'Initialization of 'System.Windows.Media.Imaging.BitmapImage' threw an exception.' Line number '1' and line position '243'.

我什至尝试使用 InlineImage 来自:http://wpftutorial.net/InlineImagesXaml.html

<InlineImage Width="100" Height="100" Stretch="Fill">
   <![CDATA[iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAB3RJTUUH2AQP
        SFlzAAALEgAACxIB0t1+/AAAAARnQU1BAACxjwv8YQUAAAnOSURBVHjaxVcLcBvVFT1vV
        ki3Hju3GCQnGjkObONQkJkxCSIHQQGnIdEr5TFs+LaGl7RRCSUvDp8nglH4mDGQ6EwZIm=]]>
    </InlineImage>

即使在这种情况下,我在 Undo/Redo 操作中也有异常。有没有可能不编写自己的 Undo/Redo 操作来处理这种情况。

我对编辑旧答案或添加新答案感到困惑。所以我要买一个新的。

使用下面的方法,我现在可以复制现有的图像文件并将其粘贴到 RTB 中,而且我现在还可以从 MSPaint、Photoshop 复制一些未保存的图像数据并将其粘贴。按保存按钮后,rtf 文件被保存并按预期在 MSWord 中打开。

Ctrl+Z 无效,因为图像数据在流中。我正在做。将图像复制为文件时,Ctrl+Z 不是问题。

欢迎大家多多提问。下面的代码是完整的,可以按原样使用。

ImageCode.cs 用于获取存储在剪贴板中的图像

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows;
using System.Runtime.InteropServices;

namespace WpfRichTextBox._32648134
{
    public class ImageCode
    {
        public static ImageSource ImageFromClipboardDibAsSource()
        {
            MemoryStream ms = Clipboard.GetData("DeviceIndependentBitmap") as MemoryStream;
            if (ms != null)
            {
                byte[] dibBuffer = new byte[ms.Length];
                ms.Read(dibBuffer, 0, dibBuffer.Length);

                BITMAPINFOHEADER infoHeader =
                    BinaryStructConverter.FromByteArray<BITMAPINFOHEADER>(dibBuffer);

                int fileHeaderSize = Marshal.SizeOf(typeof(BITMAPFILEHEADER));
                int infoHeaderSize = infoHeader.biSize;
                int fileSize = fileHeaderSize + infoHeader.biSize + infoHeader.biSizeImage;

                BITMAPFILEHEADER fileHeader = new BITMAPFILEHEADER();
                fileHeader.bfType = BITMAPFILEHEADER.BM;
                fileHeader.bfSize = fileSize;
                fileHeader.bfReserved1 = 0;
                fileHeader.bfReserved2 = 0;
                fileHeader.bfOffBits = fileHeaderSize + infoHeaderSize + infoHeader.biClrUsed * 4;

                byte[] fileHeaderBytes =
                    BinaryStructConverter.ToByteArray<BITMAPFILEHEADER>(fileHeader);

                MemoryStream msBitmap = new MemoryStream();
                msBitmap.Write(fileHeaderBytes, 0, fileHeaderSize);
                msBitmap.Write(dibBuffer, 0, dibBuffer.Length);
                msBitmap.Seek(0, SeekOrigin.Begin);

                BitmapImage img = new BitmapImage();
                img.BeginInit();
                img.CacheOption = BitmapCacheOption.OnDemand;
                img.CreateOptions = BitmapCreateOptions.DelayCreation;
                img.StreamSource = msBitmap;
                img.EndInit();

                return img;
            }
            return null;
        }

        public static MemoryStream ImageFromClipboardDibAsStream()
        {
            MemoryStream ms = Clipboard.GetData("DeviceIndependentBitmap") as MemoryStream;
            if (ms != null)
            {
                byte[] dibBuffer = new byte[ms.Length];
                ms.Read(dibBuffer, 0, dibBuffer.Length);

                BITMAPINFOHEADER infoHeader =
                    BinaryStructConverter.FromByteArray<BITMAPINFOHEADER>(dibBuffer);

                int fileHeaderSize = Marshal.SizeOf(typeof(BITMAPFILEHEADER));
                int infoHeaderSize = infoHeader.biSize;
                int fileSize = fileHeaderSize + infoHeader.biSize + infoHeader.biSizeImage;

                BITMAPFILEHEADER fileHeader = new BITMAPFILEHEADER();
                fileHeader.bfType = BITMAPFILEHEADER.BM;
                fileHeader.bfSize = fileSize;
                fileHeader.bfReserved1 = 0;
                fileHeader.bfReserved2 = 0;
                fileHeader.bfOffBits = fileHeaderSize + infoHeaderSize + infoHeader.biClrUsed * 4;

                byte[] fileHeaderBytes =
                    BinaryStructConverter.ToByteArray<BITMAPFILEHEADER>(fileHeader);

                MemoryStream msBitmap = new MemoryStream();
                msBitmap.Write(fileHeaderBytes, 0, fileHeaderSize);
                msBitmap.Write(dibBuffer, 0, dibBuffer.Length);
                msBitmap.Seek(0, SeekOrigin.Begin);

                return msBitmap;
            }
            return null;
        }
    }

    public static class BinaryStructConverter
    {
        public static T FromByteArray<T>(byte[] bytes) where T : struct
        {
            IntPtr ptr = IntPtr.Zero;
            try
            {
                int size = Marshal.SizeOf(typeof(T));
                ptr = Marshal.AllocHGlobal(size);
                Marshal.Copy(bytes, 0, ptr, size);
                object obj = Marshal.PtrToStructure(ptr, typeof(T));
                return (T)obj;
            }
            finally
            {
                if (ptr != IntPtr.Zero)
                    Marshal.FreeHGlobal(ptr);
            }
        }

        public static byte[] ToByteArray<T>(T obj) where T : struct
        {
            IntPtr ptr = IntPtr.Zero;
            try
            {
                int size = Marshal.SizeOf(typeof(T));
                ptr = Marshal.AllocHGlobal(size);
                Marshal.StructureToPtr(obj, ptr, true);
                byte[] bytes = new byte[size];
                Marshal.Copy(ptr, bytes, 0, size);
                return bytes;
            }
            finally
            {
                if (ptr != IntPtr.Zero)
                    Marshal.FreeHGlobal(ptr);
            }
        }
    }

    [StructLayout(LayoutKind.Sequential, Pack = 2)]
     struct BITMAPFILEHEADER
    {
        public static readonly short BM = 0x4d42; // BM

        public short bfType;
        public int bfSize;
        public short bfReserved1;
        public short bfReserved2;
        public int bfOffBits;
    }

    [StructLayout(LayoutKind.Sequential)]
     struct BITMAPINFOHEADER
    {
        public int biSize;
        public int biWidth;
        public int biHeight;
        public short biPlanes;
        public short biBitCount;
        public int biCompression;
        public int biSizeImage;
        public int biXPelsPerMeter;
        public int biYPelsPerMeter;
        public int biClrUsed;
        public int biClrImportant;
    }

}

MainWindow.xaml

<Window x:Class="WpfRichTextBox._32648134.Win32648134"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Win32648134" Height="600" Width="700">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="41*"/>
            <RowDefinition Height="5*"/>
            <RowDefinition Height="21*"/>
        </Grid.RowDefinitions>
        <RichTextBox x:Name="RtbCompose" Width="500" Height="300" ScrollViewer.VerticalScrollBarVisibility="Visible">
            <FlowDocument x:Name="FdDocument">
                <Paragraph x:Name="Para1"></Paragraph>
            </FlowDocument>
        </RichTextBox>        
        <Button x:Name="BtnCopyImgFile"  Content="Paste image" HorizontalAlignment="Left" Margin="96,10,0,0" Grid.Row="1" VerticalAlignment="Top" Width="75" Click="BtnCopyImgFile_Click"/>
        <Button x:Name="BtnSave" Content="Save" HorizontalAlignment="Left" Grid.Row="1" VerticalAlignment="Top" Width="75" Margin="521,10,0,0" Click="BtnSave_Click"/>
        <Button x:Name="BtnCopyImgData" Content="Paste image data" HorizontalAlignment="Left" Margin="190,11,0,0" Grid.Row="1" VerticalAlignment="Top" Click="BtnCopyImgData_Click"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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.Shapes;
using System.Diagnostics;
using System.IO;

namespace WpfRichTextBox._32648134
{
    /// <summary>
    /// Interaction logic for Win32648134.xaml
    /// </summary>
    public partial class Win32648134 : Window
    {
        public Win32648134()
        {
            InitializeComponent();
        }

        private void BtnCopyImgFile_Click(object sender, RoutedEventArgs e)
        {              
            Image i = new Image();

            if (Clipboard.ContainsFileDropList())
            {
                StringCollection fileNames = Clipboard.GetFileDropList();
                BitmapImage img = new BitmapImage(new Uri(fileNames[0], UriKind.Absolute));
                i.Source = img;
                Para1.Inlines.Add(i);
            }     

            Para1.Inlines.Add(new Run("first rtb app"));
        }

        private void BtnSave_Click(object sender, RoutedEventArgs e)
        {
            TextRange allText = new TextRange(RtbCompose.Document.ContentStart, RtbCompose.Document.ContentEnd);

            FileStream stream = new FileStream(@"I:\RTB.rtf", FileMode.Create);

            allText.Save(stream, DataFormats.Rtf);

            if (stream != null)
                stream.Close();
        }

        private void BtnCopyImgData_Click(object sender, RoutedEventArgs e)
        {
            bool hasImgData = Clipboard.ContainsImage();
            Image i = new Image();
            if (hasImgData)
            {
                BitmapSource imgData = (BitmapSource)ImageCode.ImageFromClipboardDibAsSource();
                i.Source = imgData;

                Para1.Inlines.Add(i);
            }

            Para1.Inlines.Add(new Run("rtb app, image comes from image data instead of file"));
        }
    }
}

我找到了解决方案。我不得不从 RichTextBox 源代码中复制 class WpfLoad。此代码将包保存为流,将图像作为内容,将 Uri 作为文档的源。

因为 class WpfPayload 是内部的,所以我无权访问此 class。我需要创建自己的。

下面是 class WpfPayLoad 的来源。

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Markup;
using System.Windows.Media.Imaging;

namespace YATE
{
    internal class WpfPayload
    {
        private const string XamlPayloadDirectory = "/Xaml"; // 
        private const string XamlEntryName = "/Document.xaml"; // 
        private const string XamlContentType = "application/vnd.ms-wpf.xaml+xml";
        private const string XamlImageName = "/Image"; // 
        private const string XamlRelationshipFromPackageToEntryPart = "http://schemas.microsoft.com/wpf/2005/10/xaml/entry";
        private const string XamlRelationshipFromXamlPartToComponentPart = "http://schemas.microsoft.com/wpf/2005/10/xaml/component";

        internal const string ImageBmpContentType = "image/bmp";
        private const string ImageGifContentType = "image/gif";
        private const string ImageJpegContentType = "image/jpeg";
        private const string ImageTiffContentType = "image/tiff";
        private const string ImagePngContentType = "image/png";

        private const string ImageBmpFileExtension = ".bmp";
        private const string ImageGifFileExtension = ".gif";
        private const string ImageJpegFileExtension = ".jpeg";
        private const string ImageJpgFileExtension = ".jpg";
        private const string ImageTiffFileExtension = ".tiff";
        private const string ImagePngFileExtension = ".png";

        Package _package = null;

        private static BitmapEncoder GetBitmapEncoder(string imageContentType)
        {
            BitmapEncoder bitmapEncoder;

            switch (imageContentType)
            {
                case ImageBmpContentType:
                    bitmapEncoder = new BmpBitmapEncoder();
                    break;
                case ImageGifContentType:
                    bitmapEncoder = new GifBitmapEncoder();
                    break;
                case ImageJpegContentType:
                    bitmapEncoder = new JpegBitmapEncoder();
                    // 

                    break;
                case ImageTiffContentType:
                    bitmapEncoder = new TiffBitmapEncoder();
                    break;
                case ImagePngContentType:
                    bitmapEncoder = new PngBitmapEncoder();
                    break;
                default:
                    bitmapEncoder = null;
                    break;
            }
            return bitmapEncoder;
        }

        // Returns a file extension corresponding to a given imageContentType
        private static string GetImageFileExtension(string imageContentType)
        {
            string imageFileExtension;
            switch (imageContentType)
            {
                case ImageBmpContentType:
                    imageFileExtension = ImageBmpFileExtension;
                    break;
                case ImageGifContentType:
                    imageFileExtension = ImageGifFileExtension;
                    break;
                case ImageJpegContentType:
                    imageFileExtension = ImageJpegFileExtension;
                    break;
                case ImageTiffContentType:
                    imageFileExtension = ImageTiffFileExtension;
                    break;
                case ImagePngContentType:
                    imageFileExtension = ImagePngFileExtension;
                    break;
                default:
                    imageFileExtension = null;
                    break;
            }
            return imageFileExtension;
        }

        WpfPayload(Package p = null)
        {
            this._package = p;
        }

        private Package CreatePackage(Stream stream)
        {
            _package = Package.Open(stream, FileMode.Create, FileAccess.ReadWrite);
            return _package;
        }


        // Generates a image part Uri for the given image index
        private static string GetImageName(int imageIndex, string imageContentType)
        {
            string imageFileExtension = GetImageFileExtension(imageContentType);
            return XamlImageName + (imageIndex + 1) + imageFileExtension;
        }

        // Generates a relative URL for using from within xaml Image tag.
        private static string GetImageReference(string imageName)
        {
            return "." + imageName; // imageName is supposed to be created by GetImageName method
        }

        private PackagePart CreateWpfEntryPart()
        {
            // Define an entry part uri
            Uri entryPartUri = new Uri(XamlPayloadDirectory + XamlEntryName, UriKind.Relative);

            // Create the main xaml part
            PackagePart part = _package.CreatePart(entryPartUri, XamlContentType, CompressionOption.Normal);
            // Compression is turned off in this mode.
            //NotCompressed = -1,
            // Compression is optimized for a resonable compromise between size and performance. 
            //Normal = 0,
            // Compression is optimized for size. 
            //Maximum = 1,
            // Compression is optimized for performance. 
            //Fast = 2 ,
            // Compression is optimized for super performance. 
            //SuperFast = 3,

            // Create the relationship referring to the entry part
            PackageRelationship entryRelationship = _package.CreateRelationship(entryPartUri, TargetMode.Internal, XamlRelationshipFromPackageToEntryPart);

            return part;
        }

        private void CreateImagePart(PackagePart sourcePart, BitmapSource imageSource, string imageContentType, int imageIndex)
        {
            // Generate a new unique image part name
            string imagePartUriString = GetImageName(imageIndex, imageContentType);

            // Define an image part uri
            Uri imagePartUri = new Uri(XamlPayloadDirectory + imagePartUriString, UriKind.Relative);

            // Create a part for the image
            PackagePart imagePart = _package.CreatePart(imagePartUri, imageContentType, CompressionOption.NotCompressed);

            // Create the relationship referring from the enrty part to the image part
            PackageRelationship componentRelationship = sourcePart.CreateRelationship(imagePartUri, TargetMode.Internal, XamlRelationshipFromXamlPartToComponentPart);

            // Encode the image data
            BitmapEncoder bitmapEncoder = GetBitmapEncoder(imageContentType);
            bitmapEncoder.Frames.Add(BitmapFrame.Create(imageSource));

            // Save encoded image data into the image part in the package
            Stream imageStream = imagePart.GetStream();
            using (imageStream)
            {
                bitmapEncoder.Save(imageStream);
            }
        }


        internal PackagePart GetWpfEntryPart()
        {
            PackagePart wpfEntryPart = null;

            // Find a relationship to entry part
            PackageRelationshipCollection entryPartRelationships = _package.GetRelationshipsByType(XamlRelationshipFromPackageToEntryPart);
            PackageRelationship entryPartRelationship = null;
            foreach (PackageRelationship packageRelationship in entryPartRelationships)
            {
                entryPartRelationship = packageRelationship;
                break;
            }

            // Get a part referred by this relationship
            if (entryPartRelationship != null)
            {
                // Get entry part uri
                Uri entryPartUri = entryPartRelationship.TargetUri;

                // Get the enrty part
                wpfEntryPart = _package.GetPart(entryPartUri);
            }

            return wpfEntryPart;
        }



        [System.Security.SecurityCritical]
        internal static Stream SaveImage(BitmapSource bitmapSource, string imageContentType)
        {
            MemoryStream stream = new MemoryStream();
            // Create the wpf package in the stream
            WpfPayload wpfPayload = new WpfPayload();
            using (wpfPayload.CreatePackage(stream))
            {
                PackagePart xamlEntryPart = wpfPayload.CreateWpfEntryPart();

                Stream xamlPartStream = xamlEntryPart.GetStream();
                using (xamlPartStream)
                {
                    int imageIndex = 0;
                    string imageReference = GetImageReference(GetImageName(imageIndex, imageContentType));

                    StreamWriter xamlPartWriter = new StreamWriter(xamlPartStream);
                    using (xamlPartWriter)
                    {
                        string xamlText =
                            "<Span xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">" +
                            "<InlineUIContainer><Image " +
                            "Width=\"" +
                            bitmapSource.Width + "\" " +
                            "Height=\"" +
                            bitmapSource.Height + "\" " +
                            "><Image.Source><BitmapImage CacheOption=\"OnLoad\" UriSource=\"" +
                            imageReference +
                            "\"/></Image.Source></Image></InlineUIContainer></Span>";
                        xamlPartWriter.Write(xamlText);
                    }
                    wpfPayload.CreateImagePart(xamlEntryPart, bitmapSource, imageContentType, imageIndex);
                }
            }
            return stream;
        }

        static int _wpfPayloadCount; // used to disambiguate between all acts of loading from different WPF payloads.


        internal static object LoadElement(Stream stream)
        {
            Package package = Package.Open(stream, FileMode.Open, FileAccess.Read);
            WpfPayload wpfPayload = new WpfPayload(package);

            PackagePart xamlEntryPart = wpfPayload.GetWpfEntryPart();


            int newWpfPayoutCount = _wpfPayloadCount++;
            Uri payloadUri = new Uri("payload://wpf" + newWpfPayoutCount, UriKind.Absolute);
            Uri entryPartUri = PackUriHelper.Create(payloadUri, xamlEntryPart.Uri); // gives an absolute uri of the entry part
            Uri packageUri = PackUriHelper.GetPackageUri(entryPartUri); // extracts package uri from combined package+part uri
            PackageStore.AddPackage(packageUri, wpfPayload.Package); // Register the package
            ParserContext parserContext = new ParserContext();
            parserContext.BaseUri = entryPartUri;

            object xamlObject = XamlReader.Load(xamlEntryPart.GetStream(), parserContext);
            // Remove the temporary uri from the PackageStore
            PackageStore.RemovePackage(packageUri);

            return xamlObject;
        }

        public Package Package
        {
            get { return _package; }
        }

    };
}

接下来如果我们有以下 RichTextBox:

<RichTextBox x:Name="RtbCompose" Width="500" Height="300">
            <FlowDocument x:Name="FdDocument">
                <Paragraph x:Name="Para1"></Paragraph>
            </FlowDocument>
</RichTextBox>        
<Button  Content="Paste image" HorizontalAlignment="Left" Margin="96,10,0,0" Grid.Row="1" VerticalAlignment="Top" Width="75" Click="Button_Click"/>

在 Button_Click 我们可以从剪贴板保存我们的图像。首先我得到了包的流,接下来我们可以将其转换为 XamlReader.Load().

 private void Button_Click(object sender, RoutedEventArgs e)
    {
        BitmapSource image = Clipboard.GetImage();
        Stream packagedImage = WpfPayload.SaveImage(image, WpfPayload.ImageBmpContentType);
        object element = WpfPayload.LoadElement(packagedImage);
        Para1.Inlines.Add(element as Span);
    }

我们可以用 XAMLPackage 保存结果。

   public byte[] SaveAllContent(RichTextBox rtb)
    {
        var content = new TextRange(rtb.Document.ContentStart, rtb_Main.Document.ContentEnd);
        using (MemoryStream ms = new MemoryStream())
        {
            content.Save(ms, DataFormats.XamlPackage, true);
            return ms.ToArray();
        }

    }

    public void LoadAllContent(byte [] bd, RichTextBox rtb)
    {
        var content = new TextRange(rtb.Document.ContentStart, rtb_Main.Document.ContentEnd);
        MemoryStream ms = new MemoryStream(bd);
        content.Load(ms, System.Windows.DataFormats.XamlPackage);
    }

使用此解决方案,Undo() 和 Redo() 工作正常:)