1

Столкнулся с проблемой при попытке установки изображения из буфера обмена. Вот минимальный код для воспроизведения проблемы.

 public MainWindow()
        {
            InitializeComponent();
            _ = new System.Threading.Timer(new System.Threading.TimerCallback((_) => 
            {               
                Application.Current.Dispatcher.Invoke(() => 
                {
                    if (Clipboard.ContainsImage() is true) 
                    {
                        BitmapSource image = Clipboard.GetImage();
                    this.Background = new ImageBrush(image);
                }
            });
        }), null, 10000, Timeout.Infinite);
    }

Ожидаемой поведение - любое изображение из буфера обмена windows должно стать фоном окна.

По факту если мы используем к примеру инструмент ms-screenclip:, тот что вызывается комбинацией горячих клавиш Win + Shift + S в Windows 10, изображение становится фоном окна. Если же открыть в Paint изображение, и попытаться скопировать область изображения, то вместо фона, в wpf окне, всё заливается чёрным цветом. Например из Paint в фотошоп нормально копируется.

Метаданные изображения вроде одинаковые, ну кроме размера самого изображения.

  • 1
    Решение однозначно рабочее. Просто я ещё в процессе алгоритма дешифровки DIB разбираюсь. – Viсtor Vovnenko May 20 '23 at 14:39

1 Answers1

1

Очень интересная задачка оказалась. Дело в том, что Paint в буфер обмена загоняет картинку только в формате DIB (Device Independent Bitmap), который несовместим с .NET оберткой буфера. Возможно через COM можно как-то его заставить преобразовать данные во что-то другое, но напрямую ничего подобного в буфере нет, и у меня не получилось достать. Так что на гениальность не претендую.

Я нашел древнее решение и немного переписал, выбросив лишнее. Для работы класса требуется установка NuGet пакета System.Drawing.Common.

Получился вот такой конвертер

public static class ClipboardDibConverter
{
    public const string Dib = "DeviceIndependentBitmap";
public static MemoryStream ConvertToPng(MemoryStream ms)
{
    if (ms == null)
        throw new ArgumentNullException(nameof(ms));
    ReadOnlySpan<byte> bytes = ms.ToArray();

    int headerSize = BinaryPrimitives.ReadInt32LittleEndian(bytes);
    // Only supporting 40-byte DIB from clipboard
    if (headerSize != 40)
        throw new ArgumentException("Unsupported DIB header size");
    ReadOnlySpan<byte> header = bytes[..headerSize];
    int dataOffset = headerSize;
    int width = BinaryPrimitives.ReadInt32LittleEndian(header[0x04..]);
    int height = BinaryPrimitives.ReadInt32LittleEndian(header[0x08..]);
    short planes = BinaryPrimitives.ReadInt16LittleEndian(header[0x0C..]);
    short bitCount = BinaryPrimitives.ReadInt16LittleEndian(header[0x0E..]);
    //Compression: 0 = RGB; 3 = BITFIELDS.
    int compression = BinaryPrimitives.ReadInt32LittleEndian(header[0x10..]);
    // Not dealing with non-standard formats.
    if (planes != 1 || (compression != 0 && compression != 3))
        throw new ArgumentException("Unsupported DIB compression type");
    PixelFormat fmt = bitCount switch
    {
        32 => PixelFormat.Format32bppRgb,
        24 => PixelFormat.Format24bppRgb,
        16 => PixelFormat.Format16bppRgb555,
        _ => throw new ArgumentException("Unsupported DIB pixel format")
    };
    if (compression == 3)
        dataOffset += 12;
    if (bytes.Length < dataOffset)
        throw new ArgumentException("Wrong DIB image data length");
    byte[] image = bytes[dataOffset..].ToArray();
    if (compression == 3)
    {
        uint redMask = BinaryPrimitives.ReadUInt32LittleEndian(bytes[headerSize..]);
        uint greenMask = BinaryPrimitives.ReadUInt32LittleEndian(bytes[(headerSize + 4)..]);
        uint blueMask = BinaryPrimitives.ReadUInt32LittleEndian(bytes[(headerSize + 8)..]);

        if (bitCount == 32 && redMask == 0xFF0000 && greenMask == 0x00FF00 && blueMask == 0x0000FF)
        {
            for (int pix = 3; pix < image.Length; pix += 4)
            {
                if (image[pix] != 0)
                {
                    fmt = PixelFormat.Format32bppPArgb;
                    break;
                }
            }
        }
        else
            throw new ArgumentException("Unsupported DIB pixel bitmask format");
    }
    using Bitmap bmp = CreateBitmap(image, width, height, fmt);
    bmp.RotateFlip(RotateFlipType.Rotate180FlipX);
    MemoryStream result = new MemoryStream();
    bmp.Save(result, ImageFormat.Png);
    return result;
}

private static Bitmap CreateBitmap(byte[] bytes, int width, int height, PixelFormat pixelFormat)
{
    Bitmap bmp = new Bitmap(width, height, pixelFormat);
    BitmapData bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.WriteOnly, bmp.PixelFormat);
    Marshal.Copy(bytes, 0, bmpData.Scan0, height * bmpData.Stride);
    bmp.UnlockBits(bmpData);
    return bmp;
}

}

Далее, я не понял, что делает ваш таймер, наверное просто ждет 10 секунд и хватает буфер обмена. Это было неудобно, поэтому я взял ранее написанный мной монитор, и написал немного другой пример.

Монитор буфера обмена

internal static class NativeMethods
{
    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool AddClipboardFormatListener(IntPtr hwnd);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool RemoveClipboardFormatListener(IntPtr hwnd);

}

public sealed class ClipboardMonitor : IDisposable
{
    private const int WM_CLIPBOARDUPDATE = 0x031D;
    private readonly HwndSourceHook _hook;
    private readonly HwndSource _hwndSource;
    private bool _disposed;

    /// <summary>
    /// Возникает в случае, если содержимое буфера обмена изменилось.
    /// </summary>
    public event EventHandler ClipboardChanged;

    public ClipboardMonitor(HwndSource hwndSource)
    {
        _hwndSource = hwndSource ?? throw new ArgumentNullException(nameof(hwndSource));
        if (!NativeMethods.AddClipboardFormatListener(_hwndSource.Handle))
            throw new Win32Exception(Marshal.GetLastWin32Error());
        _hook = WndProc;
        _hwndSource.AddHook(_hook);
    }

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        switch (msg)
        {
            case WM_CLIPBOARDUPDATE:
                ClipboardChanged?.Invoke(this, EventArgs.Empty);
                handled = true;
                break;
            default:
                handled = false;
                break;
        }
        return IntPtr.Zero;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (_disposed)
            return;
        _disposed = true;
        if (disposing)
            _hwndSource.RemoveHook(_hook);
        if (!NativeMethods.RemoveClipboardFormatListener(_hwndSource.Handle) && disposing)
            throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    ~ClipboardMonitor() => Dispose(false);
}

XAML

<Window x:Class="WpfAppClipImage.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfAppClipImage"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
</Window>

Кодбихайнд

public partial class MainWindow : Window
{
    private ClipboardMonitor _monitor;
public MainWindow()
{
    InitializeComponent();
    SourceInitialized += MainWindow_SourceInitialized;
    Closing += MainWindow_Closing;            
}

private void MainWindow_SourceInitialized(object? sender, EventArgs e)
{
    HwndSource hwnd = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
    _monitor = new ClipboardMonitor(hwnd);
    _monitor.ClipboardChanged += OnClipboardChanged;
}

private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
    _monitor?.Dispose();
}

private void OnClipboardChanged(object? sender, EventArgs e)
{
    try
    {
        IDataObject obj = Clipboard.GetDataObject();
        string[] formats = obj.GetFormats(false);
        if (formats.Contains(ClipboardDibConverter.Dib, StringComparer.Ordinal))
        {
            object data = obj.GetData(ClipboardDibConverter.Dib, false);
            if (data is MemoryStream ms)
            {
                MemoryStream png = ClipboardDibConverter.ConvertToPng(ms);
                BitmapImage image = BitmapImageFromStream(png);
                Background = new ImageBrush(image);
            }
        }
        else if (Clipboard.ContainsImage())
        {
            object data = obj.GetData(DataFormats.Bitmap);
            if (data is BitmapSource src)
            {
                Background = new ImageBrush(src);
            }
        }
    }
    catch (Exception ex)
    {
        Debug.Fail(ex.Message, ex.ToString());
    }
}

public static BitmapImage BitmapImageFromStream(MemoryStream ms)
{
    BitmapImage image = new BitmapImage();
    image.BeginInit();
    image.StreamSource = ms;
    image.EndInit();
    return image;
}

}

Вся суть ответа в методе OnClipboardChanged. Если в буфере обмена DIB, то беру его, в противном случае использую ваш метод из вопроса.

Кстати, при попытке прочитать буфер обмена, может вывалиться исключение о том, что буфер обмена не получилось открыть, это нормально, в таких случаях надо попытаться еще раз. Я обычно такое исключение ловлю в цикле и пытаюсь примерно 10 попыток раз в 50мс, и если все равно не удалось, то выбрасываю наверх последнее. Такое исключение случается не часто, но по-другому его не объехать.

aepot
  • 49,560