我们通过名为“bPac”的编程接口/SDK 使用 Brother 标签打印机(QL 系列)。该工具(标签打印功能是该工具的一部分)目前正在从 Ruby 重写为 Rust。在这个过程中,我陷入了 Rust 中的 Win32/COM/OLE 问题。 Ruby 中的最小工作示例非常简单:
doc = WIN32OLE.new "bpac.Document"
doc.open 'some_label.lbx'
doc.SetPrinter "Brother QL-810W", true
print_opts = 0
doc.StartPrint("", print_opts)
doc.PrintOut(1, print_opts)
doc.EndPrint
我想从 Rust 中有一个类似的简单工作示例开始。由于我不熟悉 Windows API,因此
windows-rs
箱子相当庞大。我想,我可能需要其中的 System::Com
部分。这就是我开始的地方:
use windows::Win32::System::{Com, Ole};
use ::windows::core::Result;
pub fn print() {
unsafe { Com::CoInitializeEx(std::ptr::null(), Com::COINIT_APARTMENTTHREADED) }.unwrap();
let clsid = unsafe { Com::CLSIDFromProgID("bpac.Document") };
println!("We've got a CLSID: {:?}", clsid);
let obj: Result<Com::IDispatch> = unsafe { Com::CoCreateInstance(&clsid.unwrap(), None, Com::CLSCTX_ALL) };
println!("IDispatch: {:?}", obj);
}
这样我就可以获得一个
IDispatch
对象,我应该能够查询可用的方法和属性。调用这个低级(非常接近 C-metal)API 时遇到问题。我找到了 win-idispatch 板条箱,但这似乎与 windows-rs
不起作用...:-/
我想做类似的事情:使用 Rust 的 COM Office/Excel 自动化。
简而言之,我围绕
IDispatch::GetIDsOfNames()
和 IDispatch::Invoke()
构建了一个包装器,对于参数,可以使用 VARIANT
。
以下资源帮助我构建了解决方案:
https://stuncloud.wordpress.com/2021/08/17/rust_com/
https://qiita.com/benki/items/42099c58e07b16293609
https://learn.microsoft.com/en-us/previous-versions/office/troubleshoot/office-developer/automate-excel-from-c
编辑 2024-03-19:添加一个快速示例:
货物.toml
[package]
name = "office_interop"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.81"
[dependencies.windows]
version = "0.54.0"
features = [
"Win32_Foundation",
"Win32_System_Com",
"Win32_System_Ole",
]
main.rs
use anyhow::{anyhow, Context};
use std::time::Duration;
use std::{env, thread};
use windows::core::*;
use windows::Win32::System::Com::*;
use windows::Win32::System::Ole::*;
const LOCALE_USER_DEFAULT: u32 = 0x0400;
const LOCALE_SYSTEM_DEFAULT: u32 = 0x0800;
//cargo run -- FILE
fn main() -> anyhow::Result<()> {
let mut args = env::args();
let _ = args.next();
let file = args.next();
let file = file.ok_or(anyhow!("arg1 missing"))?;
unsafe {
let res = CoInitialize(None);
if res.is_err() {
return Err(anyhow!("error: {}", res.message()));
}
let _com = DeferCoUninitialize;
let clsid = CLSIDFromProgID(PCWSTR::from_raw(
HSTRING::from("Excel.Application").as_ptr(),
))
.with_context(|| "CLSIDFromProgID")?;
println!("{:?}", clsid);
let excel = CoCreateInstance(&clsid, None, CLSCTX_LOCAL_SERVER)
.with_context(|| "CoCreateInstance")?;
let excel = IDispatchWrapper(excel);
let _excel = DeferExcelQuit(&excel);
excel
.put("Visible", vec![false.into()])
.with_context(|| "Visible false")?;
excel
.put("DisplayAlerts", vec![false.into()])
.with_context(|| "DisplayAlerts false")?;
let result = excel.get("Workbooks").with_context(|| "get Workbooks")?;
let workbooks = result.idispatch().with_context(|| "idispatch Workbooks")?;
let result = workbooks
.call("Open", vec![(&file).into()])
.with_context(|| format!("Failed to open file \"{}\"!", file))?;
let workbook = result.idispatch().with_context(|| "idispatch Workbook")?;
thread::sleep(Duration::from_millis(3000));
let result = excel.get("Caption").with_context(|| "get Caption")?;
println!("{}", result.string().with_context(|| "Caption string")?);
workbook
.call("Close", vec![false.into()])
.with_context(|| "call Close")?;
thread::sleep(Duration::from_millis(3000));
let result = excel.get("Caption").with_context(|| "get Caption")?;
println!("{}", result.string().with_context(|| "Caption string")?);
thread::sleep(Duration::from_millis(3000));
Ok(())
}
}
pub struct Variant(VARIANT);
impl From<bool> for Variant {
fn from(value: bool) -> Self {
Self(value.into())
}
}
impl From<i32> for Variant {
fn from(value: i32) -> Self {
Self(value.into())
}
}
impl From<&str> for Variant {
fn from(value: &str) -> Self {
Self(BSTR::from(value).into())
}
}
impl From<&String> for Variant {
fn from(value: &String) -> Self {
Self(BSTR::from(value).into())
}
}
impl Variant {
pub fn bool(&self) -> anyhow::Result<bool> {
Ok(bool::try_from(&self.0)?)
}
pub fn int(&self) -> anyhow::Result<i32> {
Ok(i32::try_from(&self.0)?)
}
pub fn string(&self) -> anyhow::Result<String> {
Ok(BSTR::try_from(&self.0)?.to_string())
}
pub fn idispatch(&self) -> anyhow::Result<IDispatchWrapper> {
Ok(IDispatchWrapper(IDispatch::try_from(&self.0)?))
}
pub fn vt(&self) -> u16 {
unsafe { self.0.as_raw().Anonymous.Anonymous.vt }
}
}
pub struct IDispatchWrapper(pub IDispatch);
impl IDispatchWrapper {
pub fn invoke(
&self,
flags: DISPATCH_FLAGS,
name: &str,
mut args: Vec<Variant>,
) -> anyhow::Result<Variant> {
unsafe {
let mut dispid = 0;
self.0
.GetIDsOfNames(
&GUID::default(),
&PCWSTR::from_raw(HSTRING::from(name).as_ptr()),
1,
LOCALE_USER_DEFAULT,
&mut dispid,
)
.with_context(|| "GetIDsOfNames")?;
let mut dp = DISPPARAMS::default();
let mut dispid_named = DISPID_PROPERTYPUT;
if !args.is_empty() {
args.reverse();
dp.cArgs = args.len() as u32;
dp.rgvarg = args.as_mut_ptr() as *mut VARIANT;
// Handle special-case for property-puts!
if (flags & DISPATCH_PROPERTYPUT) != DISPATCH_FLAGS(0) {
dp.cNamedArgs = 1;
dp.rgdispidNamedArgs = &mut dispid_named;
}
}
let mut result = VARIANT::default();
self.0
.Invoke(
dispid,
&GUID::default(),
LOCALE_SYSTEM_DEFAULT,
flags,
&dp,
Some(&mut result),
None,
None,
)
.with_context(|| "Invoke")?;
Ok(Variant(result))
}
}
pub fn get(&self, name: &str) -> anyhow::Result<Variant> {
self.invoke(DISPATCH_PROPERTYGET, name, vec![])
}
pub fn int(&self, name: &str) -> anyhow::Result<i32> {
let result = self.get(name)?;
result.int()
}
pub fn bool(&self, name: &str) -> anyhow::Result<bool> {
let result = self.get(name)?;
result.bool()
}
pub fn string(&self, name: &str) -> anyhow::Result<String> {
let result = self.get(name)?;
result.string()
}
pub fn put(&self, name: &str, args: Vec<Variant>) -> anyhow::Result<Variant> {
self.invoke(DISPATCH_PROPERTYPUT, name, args)
}
pub fn call(&self, name: &str, args: Vec<Variant>) -> anyhow::Result<Variant> {
self.invoke(DISPATCH_METHOD, name, args)
}
}
pub struct DeferExcelQuit<'a>(pub &'a IDispatchWrapper);
impl Drop for DeferExcelQuit<'_> {
fn drop(&mut self) {
let result = self.0.get("Workbooks").unwrap();
let workbooks = result.idispatch().unwrap();
let count = workbooks.int("Count").unwrap();
for i in 1..=count {
let result = workbooks
.invoke(DISPATCH_PROPERTYGET, "Item", vec![i.into()])
.unwrap();
let workbook = result.idispatch().unwrap();
workbook.put("Saved", vec![true.into()]).unwrap();
}
self.0.call("Quit", vec![]).unwrap(); //todo: quit doesn't kill process after open and Visible=true
}
}
pub struct DeferCoUninitialize;
impl Drop for DeferCoUninitialize {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}