In this post, I will be showing how you can host a WPF UserControl in a Delphi application and a Delphi VCL TFrame in a WPF application. To achieve this, we will use a Type Library. A Type Library is going to allow our C# code to talk to our Delphi code, and vice-versa. Let’s start with hosting a Delphi VCL TFrame in WPF.
Part I: Hosting Delphi in WPF
Firstly, create a new project in Delphi, under File > New > Other… expand Delphi and select Windows. Choose ActiveX Library. It will provide you with a *.ridl editor right away. Modify this IDL right away by defining your class as follows:
- Give your top-level object in the tree view a unique name such as DelphiCOMService.
- Right click DelphiCOMService and select New > Interface. This will allow us to define our COM interface which we will be able to use from C#.
- Give your interface a unique name such as IDelphiCOMService. Make it have a Parent Interface of IUnknown. There are different parent interfaces you can choose; IUnknown is the most light-weight for our purposes.
- Right click the interface and add a couple of new methods. One will be called GetDelphiFrame, the other WindowResized.
- Set up their parameters by modifying their Parameters tab.
- GetDelphiFrame will return a void* as its Return Type. It will take two parameters, a width and a height parameter with each of them having an [in] modifier, both of which will be a double. It will be called to instantiate our VCL TFrame and return back a pointer to it for consumption in WPF.
- WindowResized will be the same except its Return Type will be an HRESULT. It will be called when the WPF window is resized and we need to resize our VCL TFrame.
- Right click DelphiCOMService and select New > CoClass. Name it DelphiCOMServiceImplementation. Give it a unique GUID such as {D448873F-EAF7-4F40-8BC7-EF9853E64A0F}. Under the Implements tab, add the IDelphiCOMService interface. This tells Delphi what class we are going to eventually create inside of a new Delphi unit that will house our interface. Hence, this is exactly what a CoClass is, as per the name.
Create the following Delphi unit named DelphiCOMServiceUnit.pas:
unit DelphiCOMServiceUnit;
interface
uses SysUtils, ComObj, ComServ, DelphiCOMService_TLB, Winapi.ActiveX, StdVcl, Frame, Vcl.Forms, Winapi.Windows, Form;
type
DelphiCOMServiceImplementation = class(TComObject, IDelphiCOMService)
private
MainFrame: TMainFrame;
MainHandle: Cardinal;
MainPointer: Pointer;
MainForm: TMainForm;
protected
function GetDelphiFrame(width: Double; height: Double): Pointer; stdcall;
procedure WindowResized(width: Double; height: Double); safecall;
end;
implementation
function DelphiCOMServiceImplementation.GetDelphiFrame(width: Double; height: Double): Pointer; stdcall;
begin
MainForm := TMainForm.Create(Application);
MainForm.ClientWidth := Trunc(width);
MainForm.ClientHeight := Trunc(height);
MainHandle := MainForm.Frame.Handle;
MainPointer := System.Pointer(MainHandle);
Result := MainPointer;
end;
procedure DelphiCOMServiceImplementation.WindowResized(width: Double; height: Double); safecall;
begin
MainForm.ClientWidth := Trunc(width);
MainForm.ClientHeight := Trunc(height);
end;
initialization
TComObjectFactory.Create(ComServer, DelphiCOMServiceImplementation, StringToGUID('{D448873F-EAF7-4F40-8BC7-EF9853E64A0F}'), 'DelphiCOMServiceImplementation', '', ciMultiInstance, tmApartment);
end.
Add the following VCL Form to the project:
unit Form;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Frame;
type
TMainForm = class(TForm)
MainFrame: TMainFrame;
private
{ Private declarations }
public
property Frame: TMainFrame read MainFrame;
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
end.
Add the following VCL Frame to the project:
unit Frame;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TMainFrame = class(TFrame)
FrameLabel: TLabel;
private
{ Private declarations }
public
{ Public declarations }
end;
implementation
{$R *.dfm}
end.
- Add the VCL Frame to your VCL Form and name it MainFrame. Make sure it resizes with the size of the form. Add a TLabel to your VCL Frame named FrameLabel. Throw some text in it, make sure its centered inside of the frame, and that it resizes with the size of the frame.
- Go back to your IDL. Hit Refresh Implementation, Register Type Library, and Save As Type Library File. Run the application in order to register the Delphi COM service.
What did we just do?
- We created and registered a new COM service, which we can call from C#.
- DelphiCOMServiceUnit houses the service.
- DelphiCOMServiceUnit creates a new Delphi VCL Form and returns back a pointer to the VCL Frame it houses when GetDelphiFrame is called.
- DelphiCOMServiceUnit resizes the Delphi VCL Form and thus the VCL Frame as appropriate when the WindowResized function is called.
- DelphiCOMServiceUnit implements IUnknown, which is why it subclasses TComObject. It also uses TComObjectFactory.Create in order to tell Delphi to register this unit as our CoClass.
- If DelphiCOMServiceUnit were to implement IDispatch, it should subclass TAutoObject and use TAutoObjectFactory.Create instead.
How do we use this new COM object?
- Create a new WPF project.
- Add a reference under the COM tab to this DelphiCOMService.
- In MainWindow.xaml, add the following border:
<Border BorderThickness="0" Padding="0" Margin="0" x:Name="DelphiHostElement" SizeChanged="MainCanvas_OnSizeChanged"/>
- Handle the Activated event in MainWindow.
- Modify MainWindow.xaml.cs as follows:
using System;
using System.Runtime.InteropServices;
using System.Windows;
using DelphiCOMService;
namespace WPFApplication
{
public partial class MainWindow : Window
{
[DllImport("msvcr110.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int _fpreset();
private DelphiCOMServiceImplementation service;
public MainWindow()
{
_fpreset();
InitializeComponent();
}
private void MainWindow_OnActivated(object sender, EventArgs e)
{
if (service != null)
return;
service = new DelphiCOMServiceImplementation();
var control = service.GetDelphiFrame(DelphiHostElement.ActualWidth, DelphiHostElement.ActualHeight);
DelphiHostElement.Child = new DelphiControlHost(DelphiHostElement.ActualWidth, DelphiHostElement.ActualHeight, control);
}
private void MainCanvas_OnSizeChanged(object sender, SizeChangedEventArgs e)
{
service?.WindowResized(DelphiHostElement.ActualWidth, DelphiHostElement.ActualHeight);
}
}
}
- Add the following class in as DelphiControlHost.cs:
using System;
using System.Windows.Interop;
using System.Runtime.InteropServices;
namespace WPFApplication
{
public class DelphiControlHost : HwndHost
{
[DllImport("user32.dll")]
static extern IntPtr CreateWindowEx(int dwExStyle, string lpszClassName, string lpszWindowName, int style, int x, int y, int width, int height, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
[DllImport("user32.dll")]
static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
static extern bool DestroyWindow(IntPtr hWnd);
int height;
int width;
IntPtr child;
public DelphiControlHost(double initialWidth, double initialHeight, IntPtr hostedControl)
{
width = (int)initialWidth;
height = (int)initialHeight;
child = hostedControl;
}
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
var host = CreateWindowEx(0, "static", null, 0x40000000 | 0x10000000, 0, 0, height, width, hwndParent.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
SetParent(child, host);
ShowWindow(child, 5);
return new HandleRef(this, host);
}
protected override void DestroyWindowCore(HandleRef hwnd)
{
DestroyWindow(hwnd.Handle);
}
}
}
What is happening here?
- The border we added in is actually our Delphi VCL Frame host.
- When our window activates, it gets a handle to the Delphi frame by calling our COM object.
- We use the DelphiControlHost like a child canvas for which we create a native window.
- The DelphiControlHost takes the Delphi frame handle and make it a child of its native window.
- We then call ShowWindow to show our Delphi VCL Frame in this border.
- The OnSizeChanged event handles any VCL Frame resizing we might need from Delphi.
Part II: Hosting WPF in Delphi
- Create a new .NET Framework Class Library named WPFCOMService.
- Right click the library and select Properties.
- Under the Build tab, select Register for COM interop.
- Add the following class named WPFCOMService.cs to this class library:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
using WPFApplication;
namespace WPFCOMService
{
[Guid("264A4D5A-C680-486A-B1DD-C265EFA1C201")]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public sealed class WPFCOMService : IWPFCOMService
{
[DllImport("user32.dll")]
static extern void ShowWindow(IntPtr hWnd, int nCmdShow);
static EmbeddedUserControl UserControl;
static IntPtr UserControlPointer;
static Dispatcher UserInterfaceThread;
public void EmbedWPFWindow(IntPtr pointer, int width, int height)
{
Debugger.Launch();
UserControlPointer = pointer;
UserControl = new EmbeddedUserControl
{
Width = width,
Height = height
};
UserControl.InitializeComponent();
var param = new HwndSourceParameters
{
WindowStyle = 0x40000000 | 0x04000000 | 0x02000000,
ParentWindow = UserControlPointer
};
var hWndSource = new HwndSource(param)
{
SizeToContent = SizeToContent.WidthAndHeight,
RootVisual = UserControl
};
ShowWindow(hWndSource.Handle, 5);
UserInterfaceThread = Dispatcher.CurrentDispatcher;
}
public void WindowResized(int width, int height)
{
UserInterfaceThread?.Invoke(() =>
{
UserControl.Width = width;
UserControl.Height = height;
});
}
}
[ComVisible(true)]
[Guid("06AD88DA-7BC3-4D3B-98E4-B383C7C55E38")]
public interface IWPFCOMService
{
void EmbedWPFWindow(IntPtr pointer, int width, int height);
void WindowResized(int width, int height);
}
}
- When this project builds, it will register a COM object named WPFCOMService with the IWPFCOMService interface. The WPFCOMService.cs file houses our CoClass.
- I will add a reference in this class library project to the WPF project I created before, but you can use any WPF project.
- I added the EmbeddedUserControl, a simple user control to my referenced WPF project.
- I use this control in my two C# COM object functions, in which I initialize and keep track of a WPF UserControl and its settings.
- The EmbedWPFWindow function takes a pointer to a Delphi window and embeds the EmbeddedUserControl into this Delphi window by using HwndSourceParameters to set the Delphi window as its ParentWindow along with the appropriate child WindowStyle.
- The WindowResized function takes a width and a height from Delphi and resizes the control as appropriate.
How do we use this COM object in Delphi?
- Create a new VCL Form project in Delphi.
- Go to the Component tab and select Import Component…
- Select Import a Type Library.
- Choose WPFCOMService (you should see it if you build the class library we created above).
- Go to the Pascal code in your main form, Main.pas, and update it as follows to use our C# class library:
unit Main;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, WPFCOMService_TLB, Vcl.ExtCtrls;
type
TMainForm = class(TForm)
procedure FormActivate(Sender: TObject);
private
service: IWPFCOMService;
thread: TThread;
isMoving: Boolean;
procedure WMEnterSizeMove(var Message: TMessage) ; message WM_ENTERSIZEMOVE;
procedure WMMove(var Message: TMessage) ; message WM_MOVE;
procedure WMExitSizeMove(var Message: TMessage) ; message WM_EXITSIZEMOVE;
procedure PerformResize;
public
{ Public declarations }
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
procedure TMainForm.FormActivate(Sender: TObject);
begin
if (service = nil) then
service := CoWPFCOMService_.Create();
service.EmbedWPFWindow(Handle, ClientWidth, ClientHeight);
end;
procedure TMainForm.WMEnterSizeMove(var Message: TMessage) ;
begin
thread := TThread.CreateAnonymousThread(
procedure
begin
while (isMoving) do
begin
PerformResize;
Sleep(10);
end;
PerformResize;
end
);
isMoving := True;
thread.Start;
end;
procedure TMainForm.WMMove(var Message: TMessage) ;
begin
PerformResize;
end;
procedure TMainForm.WMExitSizeMove(var Message: TMessage) ;
begin
isMoving := False;
end;
procedure TMainForm.PerformResize;
begin
if (service = nil) then
exit;
service.WindowResized(ClientWidth, ClientHeight);
end;
end.
- Make sure to bind the Event named OnActivate of the main VCL Form to the FormActivate procedure.
Sample Projects
Finally, you can download the complete source code of my implementation. Note that, you use this code at your own risk and are entirely liable for any problems that may arise out of it, whatsoever. I do ask that if you use my code, you give me a little credit. Just throw my name in a comment somewhere. Here is the download link: Projects.zip.