Exibindo imagens com WPF

Publicado por Fabio A. Falavinha
15/4/2011
Categoria:
Tags: , ,

WPF (Windows Presentation Foundation) é a nova plataforma para desenvolvimento de interfaces gráficas (UI), proposta pela Microsoft. Um conceito inovador que combina a criação de aplicações 2D, 3D, documentos e controles multimedia.

Esta nova plataforma foi baseada no framework WIC (Windows Imaging Component), desenvolvido pela Microsoft para atuar em um nível mais baixo na manipulação de CODECs, para imagens e vídeos.

O objetivo deste artigo é mostrar como carregar dinamicamente imagens com WPF, de forma rápida e eficiente, reutilizando o mesmo objeto para ler novas imagens e exibí-las na janela da aplicação.

No Disposable?

Muitos desenvolvedores .NET já obseravam que ao trabalhar com classes GDI, devem utilizar o comando using, para garantir que os objetos sejam “descartados” pelo coletor, ou seja, garantir que o trabalho do garbage collector seja mais fácil. Mas, isso ficou um pouco de lado, devido a nova plataforma WPF não implementar em suas classes a interface IDisposable.

Porém, nem tudo está perdido! Apresentarei aqui neste artigo uma forma pouco convencional, porém eficaz, para que possamos descartar objetos criados dentro dos contextos de uma aplicação WPF.

Exibindo Imagens

Vamos dar início a primeiro passo, criando um projeto WPF, utilizando Visual Studio 2010 e .NET 4.0, para criar uma aplicação que exiba uma sequência de imagens de um diretório pré-configurado.

<Window x:Class="Test.ZBRA.UI.WPF.ImageView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Image Loader" Height="600" Width="800" SizeChanged="Window_SizeChanged" Closing="Window_Closing" WindowStartupLocation="CenterScreen" LocationChanged="Window_LocationChanged" Loaded="Window_Loaded">
    <Image Name="imageControl" Stretch="Fill" />
</Window>

O código acima descreve o arquivo XAML, que descreve o conteúdo estático de configuração da interface gráfica em WPF. Notem, que apenas criei um objeto Window com o componente de imagem Image, identificado por imageControl.

O próximo passo é carregar dinamicamente as imagens, exibindo-as de forma sequencial com intervalo de tempo de 3 segundos.

    public partial class ImageView : Window
    {
        private static readonly string imageRepositoryPath = "D:/TEMP/ImageRepository/In";
        private static readonly int interval = 3000;

        private Thread thread;

        public ImageView()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            thread = new Thread(() => LoadImages());
            thread.Start();
        }

        private void LoadImages()
        {
            string[] bitmapFiles = Directory.GetFiles(imageRepositoryPath, "*.bmp");
            while (true)
            {
                foreach (string bitmapFile in bitmapFiles)
                {
                    Dispatcher.BeginInvoke(new Action(() =>
                        {
                            imageControl.BeginInit();
                            imageControl.Source = new BitmapImage(new Uri(bitmapFile));
                            imageControl.EndInit();
                            InvalidateVisual();
                        }));

                    System.Threading.Thread.Sleep(interval);
                }

                System.Threading.Thread.Sleep(interval);
            }
        }

        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            if (thread != null)
                thread.Abort();
        }

        private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
        {
        }

        private void Window_LocationChanged(object sender, EventArgs e)
        {
        }
    }

O código acima, descreve a classe principal ImageView (Window) carregando as imagens e exibindo-as no componente de imagem. Notem, que é necessário criar um objeto BitmapImage, que é do tipo ImageSource, a partir do caminho da imagem que está no disco, onde este objeto é associado ao controle de imagem. Essa é uma solução simples, porém pouco eficaz, pois se o repositório tiver muitas imagens, a criação de objetos BitmapImage será constante e desnecessária, aumentando o consumo de memória e deixando a aplicação mais lenta.

Para resolver este problema, podemos partir para uma solução de baixo nível, mas ainda utilizando WPF, que é acessar o ponteiro de memória de um objeto ImageSource, garantido apenas uma instância para escrevermos a imagem como array de bytes.

    public partial class ImageView : Window
    {
        #region class attributes

        private static readonly string imageRepositoryPath = "D:/TEMP/ImageRepository/In";
        private static readonly int interval = 3000;

        private const int imageWidth = 3840;
        private const int imageHeight = 1080;
        private const double dpiX = 96;
        private const double dpiY = 96;

        private Thread thread;
        private IntPtr imageUnmanagedBufferPointer = IntPtr.Zero;
        private WICBitmap wicBitmap;

        #endregion

        public ImageView()
        {
            InitializeComponent();
            ConfigureImage();
        }

        private void ConfigureImage()
        {
            PixelFormat pixelFormat = PixelFormats.Bgra32;
            int stride = (imageWidth * pixelFormat.BitsPerPixel) / 8;
            int unmanagedBufferLength = stride * imageHeight;
            imageUnmanagedBufferPointer = Marshal.AllocHGlobal(unmanagedBufferLength);
            BitmapSource bitmapSource = BitmapSource.Create(imageWidth, imageHeight, dpiX, dpiY, pixelFormat, null, imageUnmanagedBufferPointer, unmanagedBufferLength, stride);
            imageControl.Source = bitmapSource;
            wicBitmap = new WICBitmap(bitmapSource);
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            thread = new Thread(() => LoadImages());
            thread.Start();
        }

        private void LoadImages()
        {
            string[] bitmapFiles = Directory.GetFiles(imageRepositoryPath, "*.bmp");
            while (true)
            {
                foreach (string bitmapFile in bitmapFiles)
                {
                    Bitmap bitmap = (Bitmap)Bitmap.FromFile(bitmapFile);
                    byte[] bitmapData = bitmap.ToByteArray();

                    Dispatcher.BeginInvoke(new Action(() =>
                        {
                            Marshal.Copy(bitmapData, 0, imageUnmanagedBufferPointer, bitmapData.Length);
                            WICBitmapLock wicBitmapLock;
                            int errorCode;
                            if (wicBitmap.TryLock(out wicBitmapLock, out errorCode))
                            {
                                if (wicBitmapLock.Pixels.CopyFrom(imageUnmanagedBufferPointer, (uint)bitmapData.Length))
                                    imageControl.InvalidateVisual();
                                wicBitmapLock.Dispose();
                            }
                            InvalidateVisual();
                        }));

                    System.Threading.Thread.Sleep(interval);
                }

                System.Threading.Thread.Sleep(interval);
            }
        }

        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            if (imageUnmanagedBufferPointer != IntPtr.Zero)
            {
                wicBitmap.Dispose();
                Marshal.FreeHGlobal(imageUnmanagedBufferPointer);
                imageUnmanagedBufferPointer = IntPtr.Zero;
            }
            if (thread != null)
                thread.Abort();
        }

        private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
        {
        }

        private void Window_LocationChanged(object sender, EventArgs e)
        {
        }
    }

O código acima, foi alterado em três partes: a configuração, exibição da imagem e dispose dos objetos.

A configuração é realizada a partir da criação de um objeto BitmapSource com o tamanho da imagem (Width e Height). Após a criação deste objeto, o mesmo é associado a estrutura de um objeto WICBitmap, no qual refere-se a toda mágica para exportar o ponteiro de dentro do objeto BitmapSource do WPF, para que possamos assim, escrever diretamente na memória alocada para a imagem.

    [Flags]
    public enum LockFlags
    {
        Sync = 1,
        Read = 2,
        Write = 4,
        ReadWrite = 6
    }

    public sealed class WICBitmap : IDisposable
    {

        private bool isDisposed = false;
        private readonly int pixelWidth;
        private readonly int pixelHeight;
        private SafeHandle wicBitmapSafeHandle;

        public WICBitmap(BitmapSource source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            this.pixelWidth = source.PixelWidth;
            this.pixelHeight = source.PixelHeight;
            Type bitmapSource = typeof(BitmapSource);
            //HACK
            //A linha abaixo faz o HACK, exportando o ponteiro do objeto BitmapSource
            FieldInfo wicSourceField = bitmapSource.GetField("_wicSource", BindingFlags.NonPublic | BindingFlags.Instance);
            wicBitmapSafeHandle = (SafeHandle)wicSourceField.GetValue(source);
        }

        public bool TryLock(out WICBitmapLock wicImageLock, out int errorCode)
        {
            return this.TryLock(pixelWidth, pixelHeight, out wicImageLock, out errorCode);
        }

        public bool TryLock(int width, int height, out WICBitmapLock wicImageLock, out int errorCode)
        {
            return this.TryLock(0, 0, pixelWidth, pixelHeight, out wicImageLock, out errorCode);
        }

        public bool TryLock(int x, int y, int width, int height, out WICBitmapLock wicImageLock, out int errorCode)
        {
            Int32Rect lockRect = new Int32Rect(x, y, width, height);
            wicImageLock = new WICBitmapLock(wicBitmapSafeHandle, ref lockRect);
            bool result = wicImageLock.Lock(wicBitmapSafeHandle, out errorCode);
            return result;
        }

        ~WICBitmap()
        {
            Dispose(false);
        }

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

        private void Dispose(bool isDisposing)
        {
            if (isDisposed)
                return;
            if (isDisposing)
            {
                wicBitmapSafeHandle.Close();
                wicBitmapSafeHandle.Dispose();
            }
            isDisposed = true;
        }
    }

Na introdução deste artigo mencionei que a plataforma WPF foi desenvolvida em cima do framework WIC. Com esta afirmação, e utilizando uma ferramenta de Debug, consegui descobrir um atributo interno da classe BitmapSource, no qual armazena o ponteiro de memória – wicBitmapSafeHandle.

Na exibição da imagem, recupero o array de bytes de cada imagem e faço a cópia para o ponteiro da memória alocado para a image pré-configurada, ou seja, através do objeto WICBitmap, faço um lock no objeto, para ter acesso exclusivo, e escrevo no buffer da imagem, via objeto WICBitmapBuffer.

    public struct WICBitmapLock : IDisposable
    {
        static WICBitmapLock()
        {
            Type internalWICBitmapType = Type.GetType("MS.Win32.PresentationCore.UnsafeNativeMethods+WICBitmap, PresentationCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
            LockMethod = internalWICBitmapType.GetMethod("Lock", BindingFlags.NonPublic | BindingFlags.Static);
            Type internalWICBitmapLockType = Type.GetType("MS.Win32.PresentationCore.UnsafeNativeMethods+WICBitmapLock, PresentationCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
            GetDataPointerMethod = internalWICBitmapLockType.GetMethod("GetDataPointer", BindingFlags.NonPublic | BindingFlags.Static);
        }

        private static readonly MethodInfo GetDataPointerMethod;
        private static readonly MethodInfo LockMethod;

        private bool isDisposed;
        private SafeHandle wicBitmapLockSafeHandle;
        private Int32Rect region;
        private WICBitmapBuffer wicBitmapBuffer;

        internal WICBitmapLock(SafeHandle wicBitmapSafeHandle, ref Int32Rect region)
        {
            this.isDisposed = false;
            this.wicBitmapLockSafeHandle = null;
            this.region = region;
            this.wicBitmapBuffer = default(WICBitmapBuffer);
        }

        internal bool Lock(SafeHandle wicBitmapSafeHandle, out int errorCode)
        {
            object[] args = new object[] { wicBitmapSafeHandle, region, LockFlags.ReadWrite, null };
            errorCode = (int)LockMethod.Invoke(null, args);
            if (errorCode != 0)
                return false;
            wicBitmapLockSafeHandle = (SafeHandle)args[3];
            args = new object[] { wicBitmapLockSafeHandle, null, null };
            errorCode = (int)GetDataPointerMethod.Invoke(null, args);
            if (errorCode != 0)
                return false;
            wicBitmapBuffer = new WICBitmapBuffer((IntPtr)args[2], (uint)args[1]);
            return true;
        }

        public WICBitmapBuffer Pixels
        {
            get
            {
                if (isDisposed)
                    throw new ObjectDisposedException("wicBitmapLockSafeHandle");
                return wicBitmapBuffer;
            }
        }

        public void Dispose()
        {
            if (isDisposed)
                return;
            wicBitmapLockSafeHandle.Close();
            wicBitmapLockSafeHandle.Dispose();
            isDisposed = true;
        }
    }
    public struct WICBitmapBuffer
    {
        [DllImport("Kernel32.dll", EntryPoint = "CopyMemory", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
        private static extern void CopyMemory(IntPtr destination, IntPtr source, uint length);

        private readonly IntPtr address;
        private readonly uint size;

        internal WICBitmapBuffer(IntPtr buffer, uint size)
        {
            this.address = buffer;
            this.size = size;
        }

        public bool CopyFrom(IntPtr source, uint size)
        {
            if (this.size > size)
                throw new ArgumentException("Size is to big to fit in buffer", "size");
            return CopyBuffer(address, source, size);
        }

        private static bool CopyBuffer(IntPtr target, IntPtr source, uint size)
        {
            CopyMemory(target, source, size);
            if (Marshal.GetLastWin32Error() != 0)
                return false;
            return true;
        }
    }

A última parte descarta os objetos e a memória alocada, como podem ver no código abaixo:

    private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        if (imageUnmanagedBufferPointer != IntPtr.Zero)
        {
            wicBitmap.Dispose();
            Marshal.FreeHGlobal(imageUnmanagedBufferPointer);
            imageUnmanagedBufferPointer = IntPtr.Zero;
        }
        if (thread != null)
            thread.Abort();
    }

Notem, que o método wicBitmap.Dispose() (código descrito acima), descarta os objetos alocados e associados ao BitmapSource do WPF, garantindo a limpeza das estruturas utilizadas para exibição de imagens.

Conclusão

A idéia deste artigo é mostrar como realizar processamento de imagens com WPF, tirando o máximo de proveito e eficiência para realizar operações, no meu caso, de renderização.

Vale ressaltar que este é um método pouco convencional para aplicações que necessitam de algo mais simples para processar imagens. Mas, para aplicações que necessitam de acesso diretamente a memória alocada aos objetos que mantém a imagem, é muito eficiente, pois a API do WPF disponibiliza classes para manipulação de objetos em alto nível, retirando a complexidade de acessos a memória, ponteiros, etc. Quanto mais alto a API for, maior a dificuldade para realizar operações de baixo nível com linguagens como C#, Java, etc. Caso, eu não tivesse acesso para exportar o ponteiro da classe BitmapSource, eu simplesmente teria que partir para uma solução em C++ ou C.

O código demonstrado neste artigo prova o conceito de eficiência e performance, porém o método utilizado é puramente um “hack” na plataforma WPF. Portanto, vale como prova de conceito, mas não como código de produção, pois qualquer mudança na versão da tecnologia WPF, pode quebrar o código demonstrado neste artigo.

Referências

1 Comentário

  • Reply

    Por Ronaldo em 9 de May de 2011 às 14:42

    Taí, gostei… Hacking!
    E a dica: “Hacking pode ser perigoso… use com parcimônia e atenção.”

    Achei “engraçado” chamar C# e Java de baixo nível, mas no caso “vale”.

    Gostei muito da análise e dos esquecidos ponteiros e acesso direto à memória.

    Parabéns.





Desenvolvido por hacklab/ com WordPress