#[cfg(not(windows))]
fn main() {
eprintln!("this example needs Windows and a registered denise_activex.dll");
}
#[cfg(windows)]
fn main() -> windows_core::Result<()> {
app::start()
}
#[cfg(windows)]
mod app {
use std::cell::{Cell, RefCell};
use std::mem::ManuallyDrop;
use denise_activex::dispatch::{self, DISPID_CHANGE, DISPID_CLICK};
use denise_activex::himetric::himetric_to_pixels;
use denise_activex::{CLSID_DENISE_PANEL, DIID_DENISE_PANEL_EVENTS};
use windows::Win32::Foundation::{
DISP_E_MEMBERNOTFOUND, E_NOTIMPL, HWND, LPARAM, LRESULT, RECT, RECTL, S_OK, SIZE,
VARIANT_FALSE, VARIANT_TRUE, WPARAM,
};
use windows::Win32::Graphics::Gdi::{
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleDC, CreateDIBSection, DIB_RGB_COLORS,
DeleteDC, DeleteObject, GdiFlush, SelectObject,
};
use windows::Win32::System::Com::{
CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx,
CoUninitialize, DISPATCH_FLAGS, DISPATCH_METHOD, DISPATCH_PROPERTYGET,
DISPATCH_PROPERTYPUT, DISPPARAMS, DVASPECT_CONTENT, EXCEPINFO, IConnectionPoint,
IConnectionPointContainer, IDispatch, IDispatch_Impl, IMoniker, IPersistStreamInit,
ITypeInfo,
};
use windows::Win32::System::Diagnostics::Debug::IObjectSafety;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Ole::{
DISPID_PROPERTYPUT, IOleClientSite, IOleClientSite_Impl, IOleContainer,
IOleInPlaceActiveObject, IOleInPlaceFrame, IOleInPlaceFrame_Impl, IOleInPlaceSite,
IOleInPlaceSite_Impl, IOleInPlaceUIWindow, IOleInPlaceUIWindow_Impl, IOleObject,
IOleWindow_Impl, IViewObject2, OLECLOSE_NOSAVE, OLEGETMONIKER, OLEINPLACEFRAMEINFO,
OLEIVERB_INPLACEACTIVATE, OLEMENUGROUPWIDTHS, OLEWHICHMK,
};
use windows::Win32::System::Variant::{
VARENUM, VARIANT, VARIANT_0, VARIANT_0_0, VARIANT_0_0_0, VT_BOOL, VT_BSTR, VariantClear,
};
use windows::Win32::UI::WindowsAndMessaging::{
CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW, GetClientRect,
GetMessageW, HMENU, MSG, PostQuitMessage, RegisterClassExW, SW_SHOW, ShowWindow,
TranslateMessage, WINDOW_EX_STYLE, WM_DESTROY, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
};
use windows_core::{
BOOL, BSTR, GUID, IUnknownImpl, Interface, OutRef, PCWSTR, Ref, implement, w,
};
pub fn start() -> windows_core::Result<()> {
unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok()? };
let result = run();
unsafe { CoUninitialize() };
result
}
fn run() -> windows_core::Result<()> {
let window = create_window()?;
println!("CoCreateInstance...");
let object: IOleObject =
unsafe { CoCreateInstance(&CLSID_DENISE_PANEL, None, CLSCTX_INPROC_SERVER) }?;
println!(" ok — the control exists and is an IOleObject");
let dispatch: IDispatch = object.cast().map_err(|_| stale("IDispatch"))?;
println!("\nautomation surface:");
print!("{}", dispatch::describe());
describe_from_the_library(&dispatch);
println!();
ask_what_it_claims_about_safety(&object);
draw_the_design_time_view(&object);
let site: IOleClientSite = Host {
window,
rect: RefCell::new(client_rect(window)),
}
.into();
unsafe { object.SetClientSite(&site) }?;
println!(" ok — SetClientSite");
if let Ok(persist) = object.cast::<IPersistStreamInit>() {
unsafe { persist.InitNew() }?;
println!(" ok — IPersistStreamInit::InitNew");
}
unsafe { object.SetHostNames(w!("Denise host"), w!("panel")) }?;
let rect = client_rect(window);
println!("DoVerb(OLEIVERB_INPLACEACTIVATE)...");
unsafe {
object.DoVerb(
OLEIVERB_INPLACEACTIVATE.0,
core::ptr::null(),
&site,
0,
window,
&rect,
)
}?;
println!(" ok — the control should now have a window\n");
let container: IConnectionPointContainer = object
.cast()
.map_err(|_| stale("IConnectionPointContainer"))?;
let point: IConnectionPoint =
unsafe { container.FindConnectionPoint(&DIID_DENISE_PANEL_EVENTS) }?;
let sink: IDispatch = Sink {
panel: dispatch.clone(),
clicks: Cell::new(0),
}
.into();
let cookie = unsafe { point.Advise(&sink) }?;
println!("\n ok — Advise, cookie {cookie}");
put_string(&dispatch, "Caption", "Skrevet av verten")?;
put_string(&dispatch, "Text", "hallo")?;
println!(" ok — Caption and Text assigned by name");
println!(
" Text reads back as {:?}",
get_string(&dispatch, "Text")?
);
put_bool(&dispatch, "Enabled", false)?;
println!(
" ok — Enabled = False reads back as {}",
get_bool(&dispatch, "Enabled")?
);
put_bool(&dispatch, "Enabled", true)?;
println!("\ntype in the field and press the button; close the window to quit");
unsafe {
let _ = ShowWindow(window, SW_SHOW);
}
let mut message = MSG::default();
while unsafe { GetMessageW(&mut message, None, 0, 0) }.as_bool() {
unsafe {
let _ = TranslateMessage(&message);
DispatchMessageW(&message);
}
}
unsafe {
let _ = point.Unadvise(cookie);
}
unsafe {
let _ = object.Close(OLECLOSE_NOSAVE);
let _ = object.SetClientSite(None);
}
Ok(())
}
fn describe_from_the_library(object: &IDispatch) {
let count = unsafe { object.GetTypeInfoCount() }.unwrap_or(0);
if count == 0 {
println!(
"\n no type library — either this DLL predates it, or the .tlb is not\n \
beside the DLL where registration put it. PowerShell will show no members."
);
return;
}
let info = match unsafe { object.GetTypeInfo(0, 0) } {
Ok(info) => info,
Err(e) => {
println!("\n GetTypeInfo failed: {e}");
return;
}
};
println!("\nfrom the registered type library:");
for member in dispatch::MEMBERS {
let wide: Vec<u16> = member
.name
.encode_utf16()
.chain(core::iter::once(0))
.collect();
let mut dispid = 0i32;
let found = unsafe { info.GetIDsOfNames(&PCWSTR(wide.as_ptr()), 1, &mut dispid) };
match found {
Ok(()) => println!(" {:<10} dispid {dispid}", member.name),
Err(e) => println!(" {:<10} NOT NAMED — {e}", member.name),
}
}
}
fn ask_what_it_claims_about_safety(object: &IOleObject) {
let Ok(safety) = object.cast::<IObjectSafety>() else {
println!(" no IObjectSafety — a scripting host would refuse it\n");
return;
};
println!("safety, as the registered server answers it:");
for (question, riid) in [
("untrusted script (IDispatch)", IDispatch::IID),
(
"untrusted data (IPersistStreamInit)",
IPersistStreamInit::IID,
),
("nothing claimed (IOleObject)", IOleObject::IID),
] {
let mut supported = 0u32;
let mut enabled = 0u32;
let answer =
unsafe { safety.GetInterfaceSafetyOptions(&riid, &mut supported, &mut enabled) };
match answer {
Ok(()) => println!(" {question} supported {supported:#x}, enabled {enabled:#x}"),
Err(e) => println!(" {question} no claim — {}", e.code().0),
}
}
println!();
}
fn draw_the_design_time_view(object: &IOleObject) {
let Ok(view) = object.cast::<IViewObject2>() else {
println!(
" no IViewObject2 — a form editor would show a blank rectangle.\n \
Almost certainly an older DLL still at the registered path."
);
return;
};
let extent = unsafe { view.GetExtent(DVASPECT_CONTENT, -1, core::ptr::null()) };
let Ok(extent) = extent else {
println!(" GetExtent failed");
return;
};
let width = himetric_to_pixels(extent.cx).clamp(1, 2048);
let height = himetric_to_pixels(extent.cy).clamp(1, 2048);
let Some(canvas) = Canvas::new(width, height) else {
println!(" could not allocate a device context to draw into");
return;
};
let rect = RECTL {
left: 0,
top: 0,
right: width,
bottom: height,
};
let drawn = unsafe {
view.Draw(
DVASPECT_CONTENT,
-1,
core::ptr::null_mut(),
None,
None,
canvas.dc,
Some(&rect),
None,
None,
0,
)
};
match drawn {
Ok(()) => {
println!("design-time view — {width}x{height}, no window, no site:");
println!("{}", as_text(&canvas.pixels(), width, height));
}
Err(e) => println!(" Draw failed: {e}"),
}
}
fn as_text(pixels: &[u32], width: i32, height: i32) -> String {
const RAMP: &[u8] = b" .:-=+*#%@";
const COLUMNS: i32 = 64;
let rows = (COLUMNS * height / width / 2).max(1);
let mut text = String::new();
for row in 0..rows {
text.push_str(" ");
for column in 0..COLUMNS {
let x = column * width / COLUMNS;
let y = row * height / rows;
let pixel = pixels[(y * width + x) as usize];
let (r, g, b) = ((pixel >> 16) & 0xFF, (pixel >> 8) & 0xFF, pixel & 0xFF);
let luminance = (r * 54 + g * 183 + b * 19) >> 8;
let step = (luminance as usize * (RAMP.len() - 1)) / 255;
text.push(RAMP[step] as char);
}
text.push('\n');
}
text
}
struct Canvas {
dc: windows::Win32::Graphics::Gdi::HDC,
bitmap: windows::Win32::Graphics::Gdi::HBITMAP,
previous: windows::Win32::Graphics::Gdi::HGDIOBJ,
bits: *mut u32,
count: usize,
}
impl Canvas {
fn new(width: i32, height: i32) -> Option<Self> {
let mut info = BITMAPINFO::default();
info.bmiHeader.biSize = size_of::<BITMAPINFOHEADER>() as u32;
info.bmiHeader.biWidth = width;
info.bmiHeader.biHeight = -height;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB.0;
let mut bits = core::ptr::null_mut();
let bitmap =
unsafe { CreateDIBSection(None, &info, DIB_RGB_COLORS, &mut bits, None, 0) }
.ok()?;
let dc = unsafe { CreateCompatibleDC(None) };
let previous = unsafe { SelectObject(dc, bitmap.into()) };
Some(Self {
dc,
bitmap,
previous,
bits: bits.cast(),
count: (width * height) as usize,
})
}
fn pixels(&self) -> Vec<u32> {
unsafe {
let _ = GdiFlush();
}
unsafe { core::slice::from_raw_parts(self.bits, self.count) }.to_vec()
}
}
impl Drop for Canvas {
fn drop(&mut self) {
unsafe {
SelectObject(self.dc, self.previous);
let _ = DeleteDC(self.dc);
let _ = DeleteObject(self.bitmap.into());
}
}
}
fn stale(interface: &str) -> windows_core::Error {
windows_core::Error::new(
windows::Win32::Foundation::E_NOINTERFACE,
format!(
"the registered server has no {interface}. It is almost certainly \
an older DLL still at the registered path — `cargo run` rebuilt \
this example and not the server. Build it and try again:\n\
\n cargo build -p denise-activex --release\n\
\nThe path in the registry does not change, so there is no need \
to run regsvr32 again."
),
)
}
#[implement(IDispatch)]
struct Sink {
panel: IDispatch,
clicks: Cell<u32>,
}
impl IDispatch_Impl for Sink_Impl {
fn GetTypeInfoCount(&self) -> windows_core::Result<u32> {
Ok(0)
}
fn GetTypeInfo(&self, _index: u32, _lcid: u32) -> windows_core::Result<ITypeInfo> {
Err(E_NOTIMPL.into())
}
fn GetIDsOfNames(
&self,
_riid: *const GUID,
_names: *const PCWSTR,
_count: u32,
_lcid: u32,
_out: *mut i32,
) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn Invoke(
&self,
dispid: i32,
_riid: *const GUID,
_lcid: u32,
_flags: DISPATCH_FLAGS,
_params: *const DISPPARAMS,
_result: *mut VARIANT,
_exception: *mut EXCEPINFO,
_argument_error: *mut u32,
) -> windows_core::Result<()> {
match dispid {
DISPID_CHANGE => {
println!(
" event: Change — Text is now {:?}",
get_string(&self.panel, "Text")?
);
Ok(())
}
DISPID_CLICK => {
let clicks = self.clicks.get() + 1;
self.clicks.set(clicks);
println!(" event: Click ({clicks})");
put_string(&self.panel, "Caption", &format!("Klikket {clicks} ganger"))
}
_ => Err(DISP_E_MEMBERNOTFOUND.into()),
}
}
}
fn dispid_of(object: &IDispatch, name: &str) -> windows_core::Result<i32> {
let wide: Vec<u16> = name.encode_utf16().chain(core::iter::once(0)).collect();
let name = PCWSTR(wide.as_ptr());
let mut dispid = 0i32;
unsafe { object.GetIDsOfNames(&GUID::zeroed(), &name, 1, 0, &mut dispid) }?;
Ok(dispid)
}
fn put_string(object: &IDispatch, name: &str, value: &str) -> windows_core::Result<()> {
let mut argument = variant(
VT_BSTR,
VARIANT_0_0_0 {
bstrVal: ManuallyDrop::new(BSTR::from(value)),
},
);
let result = invoke_put(object, name, &mut argument);
unsafe {
let _ = VariantClear(&mut argument);
}
result
}
fn put_bool(object: &IDispatch, name: &str, value: bool) -> windows_core::Result<()> {
let mut argument = variant(
VT_BOOL,
VARIANT_0_0_0 {
boolVal: if value { VARIANT_TRUE } else { VARIANT_FALSE },
},
);
invoke_put(object, name, &mut argument)
}
fn invoke_put(
object: &IDispatch,
name: &str,
argument: &mut VARIANT,
) -> windows_core::Result<()> {
let dispid = dispid_of(object, name)?;
let mut named = DISPID_PROPERTYPUT;
let params = DISPPARAMS {
rgvarg: argument,
rgdispidNamedArgs: &mut named,
cArgs: 1,
cNamedArgs: 1,
};
unsafe {
object.Invoke(
dispid,
&GUID::zeroed(),
0,
DISPATCH_PROPERTYPUT,
¶ms,
None,
None,
None,
)
}
}
fn get_string(object: &IDispatch, name: &str) -> windows_core::Result<String> {
let mut result = invoke_get(object, name)?;
let text = unsafe { result.Anonymous.Anonymous.Anonymous.bstrVal.to_string() };
unsafe {
let _ = VariantClear(&mut result);
}
Ok(text)
}
fn get_bool(object: &IDispatch, name: &str) -> windows_core::Result<bool> {
let result = invoke_get(object, name)?;
Ok(unsafe { result.Anonymous.Anonymous.Anonymous.boolVal.0 != 0 })
}
fn invoke_get(object: &IDispatch, name: &str) -> windows_core::Result<VARIANT> {
let dispid = dispid_of(object, name)?;
let params = DISPPARAMS::default();
let mut result = VARIANT::default();
unsafe {
object.Invoke(
dispid,
&GUID::zeroed(),
0,
DISPATCH_PROPERTYGET,
¶ms,
Some(&mut result),
None,
None,
)
}?;
Ok(result)
}
fn variant(vt: VARENUM, payload: VARIANT_0_0_0) -> VARIANT {
VARIANT {
Anonymous: VARIANT_0 {
Anonymous: ManuallyDrop::new(VARIANT_0_0 {
vt,
wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: payload,
}),
},
}
}
const _: DISPATCH_FLAGS = DISPATCH_METHOD;
fn create_window() -> windows_core::Result<HWND> {
let instance = unsafe { GetModuleHandleW(None) }?;
let class = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
lpfnWndProc: Some(host_proc),
hInstance: instance.into(),
lpszClassName: w!("Denise.Host"),
..Default::default()
};
unsafe { RegisterClassExW(&class) };
unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
w!("Denise.Host"),
w!("Denise — ActiveX container"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
520,
400,
None,
None,
Some(instance.into()),
None,
)
}
}
fn client_rect(window: HWND) -> RECT {
let mut rect = RECT::default();
unsafe {
let _ = GetClientRect(window, &mut rect);
}
rect
}
extern "system" fn host_proc(hwnd: HWND, message: u32, w: WPARAM, l: LPARAM) -> LRESULT {
if message == WM_DESTROY {
unsafe { PostQuitMessage(0) };
return LRESULT(0);
}
unsafe { DefWindowProcW(hwnd, message, w, l) }
}
#[implement(IOleClientSite, IOleInPlaceSite, IOleInPlaceFrame)]
struct Host {
window: HWND,
rect: RefCell<RECT>,
}
impl IOleClientSite_Impl for Host_Impl {
fn SaveObject(&self) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn GetMoniker(
&self,
_assign: &OLEGETMONIKER,
_which: &OLEWHICHMK,
) -> windows_core::Result<IMoniker> {
Err(E_NOTIMPL.into())
}
fn GetContainer(&self) -> windows_core::Result<IOleContainer> {
Err(E_NOTIMPL.into())
}
fn ShowObject(&self) -> windows_core::Result<()> {
Ok(())
}
fn OnShowWindow(&self, _show: BOOL) -> windows_core::Result<()> {
Ok(())
}
fn RequestNewObjectLayout(&self) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
}
impl IOleWindow_Impl for Host_Impl {
fn GetWindow(&self) -> windows_core::Result<HWND> {
Ok(self.window)
}
fn ContextSensitiveHelp(&self, _entering: BOOL) -> windows_core::Result<()> {
Ok(())
}
}
impl IOleInPlaceSite_Impl for Host_Impl {
fn CanInPlaceActivate(&self) -> windows_core::Result<()> {
Ok(())
}
fn OnInPlaceActivate(&self) -> windows_core::Result<()> {
println!(" site: OnInPlaceActivate");
Ok(())
}
fn OnUIActivate(&self) -> windows_core::Result<()> {
println!(" site: OnUIActivate");
Ok(())
}
fn GetWindowContext(
&self,
frame: OutRef<'_, IOleInPlaceFrame>,
doc: OutRef<'_, IOleInPlaceUIWindow>,
position: *mut RECT,
clip: *mut RECT,
info: *mut OLEINPLACEFRAMEINFO,
) -> windows_core::Result<()> {
let rect = *self.rect.borrow();
if !position.is_null() {
unsafe { position.write(rect) };
}
if !clip.is_null() {
unsafe { clip.write(rect) };
}
if !info.is_null() {
unsafe {
(*info).cb = size_of::<OLEINPLACEFRAMEINFO>() as u32;
(*info).fMDIApp = false.into();
(*info).hwndFrame = self.window;
(*info).haccel = Default::default();
(*info).cAccelEntries = 0;
}
}
let this: IOleInPlaceFrame = self.to_interface();
let _ = frame.write(Some(this));
let _ = doc.write(None);
Ok(())
}
fn Scroll(&self, _extent: &SIZE) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn OnUIDeactivate(&self, _undoable: BOOL) -> windows_core::Result<()> {
Ok(())
}
fn OnInPlaceDeactivate(&self) -> windows_core::Result<()> {
println!(" site: OnInPlaceDeactivate");
Ok(())
}
fn DiscardUndoState(&self) -> windows_core::Result<()> {
Ok(())
}
fn DeactivateAndUndo(&self) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn OnPosRectChange(&self, position: *const RECT) -> windows_core::Result<()> {
if !position.is_null() {
*self.rect.borrow_mut() = unsafe { *position };
}
Ok(())
}
}
impl IOleInPlaceUIWindow_Impl for Host_Impl {
fn GetBorder(&self) -> windows_core::Result<RECT> {
Err(E_NOTIMPL.into())
}
fn RequestBorderSpace(&self, _widths: *const RECT) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn SetBorderSpace(&self, _widths: *const RECT) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn SetActiveObject(
&self,
_active: Ref<'_, IOleInPlaceActiveObject>,
_name: &PCWSTR,
) -> windows_core::Result<()> {
Ok(())
}
}
impl IOleInPlaceFrame_Impl for Host_Impl {
fn InsertMenus(
&self,
_shared: HMENU,
_widths: *mut OLEMENUGROUPWIDTHS,
) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn SetMenu(&self, _shared: HMENU, _ole: isize, _active: HWND) -> windows_core::Result<()> {
Ok(())
}
fn RemoveMenus(&self, _shared: HMENU) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn SetStatusText(&self, _text: &PCWSTR) -> windows_core::Result<()> {
Ok(())
}
fn EnableModeless(&self, _enable: BOOL) -> windows_core::Result<()> {
Ok(())
}
fn TranslateAccelerator(&self, _message: *const MSG, _id: u16) -> windows_core::Result<()> {
Err(windows_core::Error::from_hresult(
windows::Win32::Foundation::S_FALSE,
))
}
}
const _: windows_core::HRESULT = S_OK;
}