Showing posts with label WPF. Show all posts
Showing posts with label WPF. Show all posts

Wednesday, September 26, 2012

Refactored WPF Auto-Complete ComboBox

Proper WPF Auto-Complete ComboBox

I had to use the Auto-Complete ComboBox included in the WPF Extended Toolkit recently. Now before I say anything else, I amongst many others appreciate the work done on this toolkit and am very greatful for it. I know that it is the culmination of work done by many competent individuals. However, the AC (Auto-Complete) ComboBox was a nightmare for my use-case.

I needed a ComboBox that:

- was Editable
- Sorts the items following each key stroke and keeps them up-to-date
- affords Data Binding to the selected item

Why was it a nightmare?

This was not easy because of the way the focus would jump from a list item to the text part of the combobox. Once a partial match was made (typed 'a' and the list contains "Aardvark", the text would be replaced with the first item in the list of matches (Aardvark). So if i was typing Apples, the 'a' would then change the text in the ComboBox to Aardvark and select it all. Then i strike 'p' and now the text in the ComboBox is wiped and replaced with 'p'... But my list also has "Plums". So now my selected text is "Plums".

And so on... (there were many use-cases where it would end up in a royal mess... users HATED this screen and would avoid it like the plague)

The first approach was to add the missing functionality from the out-of-the-box ACCB (Auto-Complete ComboBox) by extending the class and using a mixture of method overriding and event handlers. This helped early on but didn't quite cut it: there was always a case where the focus would get lost or the text was highlighted from an odd point. Thus in the end, the control had very odd behaviour (as described above).

That's when I restarted the control from scratch. I decided to use a ComboBox (non Editable) with a TextBox that sits above the Text part of the ComboBox and do the coupling myself.

However, I needed access to which item was highlighted in the dropdown when the user cycles through the possible matches. So I stood on the shoulders of giants (Christian Moser's WPF Tutorial to be exact) and use the LivePreviewComboBox which was conjured up for just that reason. When the user starts typing and the dropdown opens with matches, hitting the down button will then scroll through the items. Similarly, hitting the up key will bring the focus up the list and finally back into the textbox to type more text.




I have a custom theme so for me this sufficed. If you are not happy with the look of the control, apply a style to it! To do so, you can create a style for the class AutoCompleteComboBox and set the template to whatever you want. I used a LivePreviewComboBox on top of some rectangles to react to IsMouseOver and HasFocus. The choice is yours!

Go ahead and download the source code and sample project!




Monday, September 12, 2011

InteropBitmap in WPF

Ahoy ahoy,

Recently I found myself in a situation where I wanted to modify a bitmap on the fly and see the results immediately in WPF. Now as most WPF peeps know, to display an image in WPF is relatively straightforward. I've included a sample application that displays 2 tabs. One with an image that is just data-bound normally to a BitmapSource and another that displays an InteropBitmap in which we will regularly modify some pixels. You can download the entire VS solution as you wish. I'll only include the relevant sections of the code here for readability's sake.


Model.cs
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void BroadcastPropertyChanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}

private BitmapSource beforeImage;
public BitmapSource BeforeImage
{
get { return beforeImage; }
set
{
if (beforeImage != value)
{
beforeImage = value;
BroadcastPropertyChanged("BeforeImage");
OnBeforeImageChanged();
}
}
}

private void OnBeforeImageChanged()
{
}
}


MainWindow.xaml.cs
public partial class MainWindow : UserControl
{
public MainWindow()
{
InitializeComponent();

Model m = new Model();
this.DataContext = m;
m.BeforeImage = new BitmapImage(new Uri(@"C:\Users\Coco\Pics\001.jpg"));
}
}


- INotifyPropertyChanged is in the System.ComponentModel namespace (System.dll)
- BitmapSource is in the System.Windows.Media.Imaging namespace (PresentationCore.dll)


If I run the application I see the image in my Before tab displayed properly. Nothing is in the After tab because we have yet to create a property "AfterImage" in the Model class.




Barcelona is great, isn't it? However, it is beside the fact. To display an image we bind the Image.Source property to either a BitmapSource or a string (giving us the path of the image to display). In this case we'll just stick to the BitmapSource.


The BitmapImage (which inherits from BitmapSource) that we set as our BeforeImage does not expose any methods to edit pixels or write into them. There are two implementations (probably amongst others) of BitmapSource that allow us to do just that. Choosing the right one depends on the circumstance.

  • WriteableBitmap
  • InteropBitmap

In my case, I chose InteropBitmap (reasons to follow) and that will be the focus of the article. InteropBitmap was originally created to render Windows Forms Bitmaps in a WPF environment. But that is not why I used it. I required the ability to have direct access to the memory buffer used to store the pixels of the image and wanted the ability to modify these pixels (slots in the memory buffer) from any thread (a normal BitmapImage allows access to it only from within the thread that created it). InteropBitmap provides this and when a BitmapSource is asked for by the Image control, we call Invalidate on the InteropBitmap and return a frozen frame of the current state of the memory buffer. If this isn't clear, just read on, hopefully it will soon make more sense.


Proper use of the InteropBitmap requires us to first expose some Win32 methods to assist us in its creation. We'll use these Win32 methods to create a section of memory that we can later write to directly as our image. The code below belongs in the Model class


Model.cs
#region Extern Stuff
const uint FILE_MAP_ALL_ACCESS = 0xF001F;
const uint PAGE_READWRITE = 0x04;

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr CreateFileMapping(IntPtr hFile,
IntPtr lpFileMappingAttributes,
uint flProtect,
uint dwMaximumSizeHigh,
uint dwMaximumSizeLow,
string lpName);

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject,
uint dwDesiredAccess,
uint dwFileOffsetHigh,
uint dwFileOffsetLow,
uint dwNumberOfBytesToMap);
#endregion

private int bufferWidth = 0;
private int bufferHeight = 0;
private uint bufferByteCount = 0;

private InteropBitmap outputBitmap;
private IntPtr outputSectionPointer;
private IntPtr outputMapPointer;
private static readonly PixelFormat outputFormat = PixelFormats.Bgra32;

private void OnBeforeImageChanged()
{
if (bufferWidth != BeforeImage.PixelWidth ||
bufferHeight != BeforeImage.PixelHeight)
{
// record the new image dimensions
bufferWidth = BeforeImage.PixelWidth;
bufferHeight = BeforeImage.PixelHeight;
bufferByteCount = (uint)(bufferWidth * bufferHeight * ((outputFormat.BitsPerPixel + 7) / 8));

outputSectionPointer = CreateFileMapping(new IntPtr(-1), IntPtr.Zero, PAGE_READWRITE, 0, bufferByteCount, null);

outputMapPointer = MapViewOfFile(outputSectionPointer, FILE_MAP_ALL_ACCESS, 0, 0, bufferByteCount);
}

// copy the original image into the buffer we created for the after image
BeforeImage.CopyPixels(new Int32Rect(0, 0, bufferWidth, bufferHeight), outputMapPointer, (int)bufferByteCount, bufferWidth * 4);

// build the InteropBitmap from the memory buffer
AfterImage = Imaging.CreateBitmapSourceFromMemorySection( outputSectionPointer, bufferWidth, bufferHeight,
outputFormat, bufferWidth * 4, 0) as InteropBitmap;
}

public BitmapSource AfterImage
{
get
{
if (outputBitmap != null)
{
outputBitmap.Invalidate();
return (BitmapSource)outputBitmap.GetAsFrozen();
}

return null;
}
private set
{
outputBitmap = value as InteropBitmap;
BroadcastPropertyChanged("AfterImage");
}
}


Once we are done creating the InteropBitmap and its memory buffer, we can write to the buffer and broadcast the AfterImage PropertyChanged event so that the (data-bound) Image viewer in the After tab will update itself. The MainWindow now contains buttons to start and stop the writing to random pixels. It also contains a refresh frequency textbox which will indicate how often we want to update the UI. Be careful though, too many UI updates and you'll bog down the UI thread. This is because when we broadcast the PropertyChanged event, the databound Image viewer in the UI listens to the event (internally) and gets the Model.AfterImage property. This forces a render cycle on the InteropBitmap and then a clone of the InteropBitmap's buffer is returned. So if you're dealing with big buffers, you may start to experience lagging. So try to limit your PropertyChanged event broadcasts on the image property exposed in the model (AfterImage in our case).


The code below was used to modify random pixels in the image's memory buffer.


Model.cs
private DateTime lastImgRefreshTime = DateTime.MinValue;
private int imageRefreshFreq = 500;
private bool isWritingPixels;
private ManualResetEvent mre;

public Model()
{
isWritingPixels = false;
mre = new ManualResetEvent(isWritingPixels);
Task.Factory.StartNew(new Action(() =>
{
Random rand = new Random();
while (true)
{
// wait for the user to start the pixel writing
mre.WaitOne();

unsafe
{
Int32* pStart = (Int32*)outputMapPointer;

int x = rand.Next(bufferWidth);
int y = rand.Next(bufferHeight);

pStart[y * this.bufferWidth + x] = unchecked((int)0xFF000000) | rand.Next();
}

DateTime now = DateTime.Now;
if ((now - lastImgRefreshTime).TotalMilliseconds > imageRefreshFreq)
{
BroadcastPropertyChanged("AfterImage");
lastImgRefreshTime = now;
}
}
}));
}

internal void StartPixelWriting()
{
TogglePixelWriting();
}

private void TogglePixelWriting()
{
if (isWritingPixels)
{
mre.Reset();
}
else
{
mre.Set();
}

isWritingPixels = !isWritingPixels;
BroadcastPropertyChanged("CanStartWriting");
BroadcastPropertyChanged("CanStopWriting");
}

internal void StopPixelWriting()
{
TogglePixelWriting();
}


And now you see the end result.