Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

WPF Interoperability


A Win32 Webcam Control

Listing One contains the unmanaged C++ definition for a custom Win32 Webcam control that wraps a few DirectShow COM objects.

#if !defined(WEBCAM_H)
#define WEBCAM_H

#include <wtypes.h>

class Webcam 
{ 
public:
 static HRESULT Initialize(int width, int height);
 static HRESULT AttachToWindow(HWND hwnd);
 static HRESULT Start();
 static HRESULT Pause();
 static HRESULT Stop();
 static HRESULT Repaint();
 static HRESULT Terminate();
 static int GetWidth();
 static int GetHeight();
} ;
#endif // !defined(WEBCAM_H)
Listing One

The Webcam class is designed to work with a computer's default video capture device, so it contains a set of simple static methods for controlling this device. It is initialized with a width and height (which can be later retrieved via GetWidth and GetHeight methods). Then, after telling Webcam (via AttachToWindow) what HWND to render itself on, the behavior can be controlled with simple Start/Pause/Stop methods.

Listing Two is the implementation of the Webcam class. The implementation begins with a simple Win32 window procedure, which makes sure to repaint the video whenever a WM_ERASEBKGND message is received. Inside Initialize, a Win32 window class called WebcamClass is defined and registered, and a bunch of DirectShow-specific COM objects are created and initialized. (The Terminate method releases all of these COM objects.) AttachToWindow not only tells DirectShow which window to render on, but it sets the size of the video to match the dimensions passed to Initialize. The other methods are simple wrappers for the underlying DirectShow methods.

LRESULT WINAPI WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{ 
 switch (msg)
 { 
  case WM_ERASEBKGND:
   DefWindowProc(hwnd, msg, wParam, lParam);
   Webcam::Repaint();
   break;
  default:
   return DefWindowProc(hwnd, msg, wParam, lParam);
 } 
 return 0;
} 
HRESULT Webcam::Initialize(int width, int height)
{ 
 _width = width;
 _height = height;

 // Create and register the Window Class
 WNDCLASS wc;
 wc.style     = CS_VREDRAW | CS_HREDRAW;
 wc.lpfnWndProc  = WndProc;
 wc.cbClsExtra  = 0;
 wc.cbWndExtra  = 0;
 wc.hInstance   = GetModuleHandle(NULL);
 wc.hIcon     = LoadIcon(NULL, IDI_APPLICATION);
 wc.hCursor    = LoadCursor(NULL, IDC_ARROW);
 wc.hbrBackground = (HBRUSH)(COLOR_SCROLLBAR+1);
 wc.lpszMenuName = 0;
 wc.lpszClassName = L"WebcamClass";
 RegisterClass(&wc);

 HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
  IID_IGraphBuilder, (void **)&_graphBuilder);
 ...Create and interact with several COM objects...
 return hr;
} 
HRESULT Webcam::AttachToWindow(HWND hwnd)
{ 
 if (!_initialized || !_windowlessControl)
  return E_FAIL;
 _hwnd = hwnd;
 // Position and size the video
 RECT rcDest;
 rcDest.left = 0;
 rcDest.right = _width;
 rcDest.top = 0;
 rcDest.bottom = _height;
 _windowlessControl->SetVideoClippingWindow(hwnd);
 return _windowlessControl->SetVideoPosition(NULL, &rcDest);
} 
HRESULT Webcam::Start()
{ 
 if (!_initialized || !_graphBuilder || !_mediaControl)
  return E_FAIL;
 _graphBuilder->Render(_pin);
 return _mediaControl->Run();
} 
HRESULT Webcam::Pause()
{ 
 if (!_initialized || !_mediaControl)
  return E_FAIL;
 return _mediaControl->Pause();
} 
HRESULT Webcam::Stop()
{ 
 if (!_initialized || !_mediaControl)
  return E_FAIL;
 return _mediaControl->Stop();
} 
HRESULT Webcam::Repaint()
{ 
 if (!_initialized || !_windowlessControl)
  return E_FAIL;
 return _windowlessControl->RepaintVideo(_hwnd, GetDC(_hwnd));
} 
HRESULT Webcam::Terminate()
{ 
 HRESULT hr = Webcam::Stop();
 ...Release several COM objects...
 return hr;
} 
int Webcam::GetWidth()
{ 
 return _width;
} 
int Webcam::GetHeight()
{ 
 return _height;
}
Listing Two

Using the Webcam Control In WPF

The first step in using the Webcam control in a WPF application is to create a project that is able to "see" this unmanaged control from the WPF-specific managed code that must be written. Many options exist for integrating managed code into an unmanaged codebase. If you're comfortable with C++, using C++/CLI to seamlessly mix managed and unmanaged code is usually the best approach. This is especially true for the Webcam class because it doesn't expose any functionality outside of the DLL in which it is compiled.

Listing Three defines a WPF Window--all in C++/CLI--and uses the HwndHost type to integrate the Win32 Webcam control. Because it is using and defining managed data types, it must be compiled with the /clr compiler option.

#include "stdafx.h"
#include "Webcam.h"

#using <mscorlib.dll>
#using <PresentationFramework.dll>
#using <WindowsBase.dll>
#using <PresentationCore.dll>

using namespace System;
using namespace System::Windows;
using namespace System::Windows::Controls;
using namespace System::Windows::Interop;
using namespace System::Runtime::InteropServices;

ref class MyHwndHost : HwndHost
{ 
protected: 
 virtual HandleRef BuildWindowCore(HandleRef hwndParent) override
 { 
  HWND hwnd = CreateWindow(L"WebcamClass", // Registered class
   NULL,                 // Title
   WS_CHILD,               // Style
   CW_USEDEFAULT, 0,           // Position
   Webcam::GetWidth(),          // Width
   Webcam::GetHeight(),          // Height
   (HWND)hwndParent.Handle.ToInt32(),   // Parent
   NULL,                 // Menu
   GetModuleHandle(NULL),         // hInstance
   NULL);                 // Optional parameter
   if (hwnd == NULL)
   throw gcnew ApplicationException("CreateWindow failed!");
  Webcam::AttachToWindow(hwnd);
  return HandleRef(this, IntPtr(hwnd));
 } 
 virtual void DestroyWindowCore(HandleRef hwnd) override
 { 
  // Just a formality:
  ::DestroyWindow((HWND)hwnd.Handle.ToInt32());
 } 
} ;
ref class Window1 : Window
{ 
public:
 Window1()
 { 
  DockPanel^ panel = gcnew DockPanel();
  MyHwndHost^ host = gcnew MyHwndHost();
  Label^ label = gcnew Label();
  label->FontSize = 20;
  label->Content = "The Win32 control is docked to the left.";
  panel->Children->Add(host);
  panel->Children->Add(label);
  this->Content = panel;

  if (FAILED(Webcam::Initialize(640, 480)))
  { 
   ::MessageBox(NULL, L"Failed to communicate with a video capture device.",
    L"Error", 0);
  } 
  Webcam::Start();
 } 
 ~Window1()
 { 
  Webcam::Terminate();
 } 
} ;
Listing Three

If the implementation of Webcam::AttachToWindow in Listing Two were changed to discover the size of the HWND, the video could stretch to fill that area.

The first thing to notice about Listing Three is that it defines a subclass of HwndHost called MyHwndHost. This is necessary because HwndHost is actually an abstract class! It contains two methods that need to be overridden:

  • BuildWindowCore, in which you must return the HWND to be hosted. This is typically where initialization is done as well. The parent HWND is given to you as a parameter to this method. If you do not return a child HWND whose parent matches the passed-in parameter, WPF throws an InvalidOperationException.
  • DestroyWindowCore, which gives you the opportunity to do any cleanup/termination when the HWND is no longer needed.

For both methods, HWNDs are represented as HandleRef types. HandleRef is a lightweight wrapper (in the System.Runtime.InteropServices namespace) that ties the lifetime of the HWND to a managed object. You typically pass this as the managed object when constructing a HandleRef.

Listing Three calls the Win32 CreateWindow API inside BuildWindowCore to create an instance of the WebcamClass window that was registered in Listing Two, passing the input HWND as the parent. The HWND returned by CreateWindow is not only returned by BuildWindowCore (inside a HandleRef), but it is also passed to the Webcam::AttachToWindow method so the video is rendered appropriately. Inside DestroyWindowCore, the Win32 DestroyWindow API is called to signify the end of the HWND's lifespan.

(A typical implementation of an HwndHost subclass calls CreateWindow inside BuildWindowCore and DestroyWindow inside DestroyWindowCore. However, calling DestroyWindow isn't necessary because a child HWND is automatically destroyed by Win32 when the parent HWND is destroyed. So in Listing Three, the implementation of DestroyWindowCore could be left empty.)

Inside the Window's constructor, the MyHwndHost is instantiated and added to a DockPanel just like any other FrameworkElement. The Webcam is then initialized and the video stream is started. (For some applications, initialization of the Win32 content might need to wait until all the WPF content has been rendered. In such cases, you can perform this initialization from Window's ContentRendered event.)

Listing Four contains the final piece needed for the WPF webcam application, which is the main method that creates the Window and runs the Application. It is also compiled with the /clr option. Figure 2 is the running application. (With Visual C++'s /clr compiler option, you can compile entire projects or individual source files as managed code.

#include "Window1.h"

using namespace System;
using namespace System::Windows;
using namespace System::Windows::Media;

[STAThreadAttribute]
int main(array<System::String ^> ^args)
{ 
 Application^ application = gcnew Application();
 Window^ window = gcnew Window1();
 window->Title = "Hosting Win32 DirectShow Content in WPF";
 window->Background = Brushes::Orange;
 application->Run(window);
 return 0;
} 
Listing Four

It's tempting to simply compile entire projects as managed code, but it's usually best if you decide on a file-by-file basis what should be compiled as managed and what should be compiled as unmanaged. Otherwise, you could create extra work for yourself without any real gain.)

[Click image to view at full size]
Figure 2: Live webcam feed is embedded in the WPF window.

The /clr option works well, but it often increases build time and can sometimes require code changes. For example, .C files must be compiled as C++ under /clr, but .C files often require some syntax changes to be compiled as such. Also, managed code can't run under the Windows loader lock, so compiling DllMain (or any code called by it) as managed results in a (fortunately quite descriptive) runtime error.

Notice the gray area underneath the video stream in Figure 2. The reason this appears is that the MyHwndHost element is docked to the left side of the DockPanel in Listing Three, but the Webcam control is initialized with a fixed size of 640x480. This change is shown in the following code, with the result shown in Figure 3.

HRESULT Webcam::AttachToWindow(HWND hwnd)
{ 
 if (!_initialized || !_windowlessControl)
  return E_FAIL;
 _hwnd = hwnd;
 // Position and size the video
 RECT rcDest;
 GetClientRect(hwnd,&rcDest);
 _windowlessControl->SetVideoClippingWindow(hwnd);
 return _windowlessControl->SetVideoPosition(NULL, &rcDest);
} 

[Click image to view at full size]
Figure 3: The Webcam control, altered to fill the entire rectangle given to it.

Although the best solution for a webcam application is probably to give the HwndHost-derived element a fixed (or at least unstretched) size, it's important to understand that WPF layout applies only to the HwndHost. Within its bounds, you need to play by Win32 rules to get the layout you desire.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.