動機
下記記事のコードがだいぶ古くなっていたし、リファクタリングついでに書き直しました。
環境
Windows 11 25H2
Microsoft® Excel® for Microsoft 365 MSO (バージョン 2511)
rustc 1.92.0
windows crate 0.62.2
解説
feature flag の変更
windows crate の[features]が変更されているので、それに対応します。
Cargo.toml
[dependencies.windows]
- version = "0.39"
+ version = "0.62"
features = [
"Win32_System_Com",
"Win32_System_Ole",
- "Win32_Foundation",
+ "Win32_System_Variant",
]
COM の初期化
COM の初期化も構造体にまとめました。
src/main.rs
struct Com;
+ impl Com {
+ fn new() -> Result<Self> {
+ unsafe { CoInitialize(None).ok()? };
+ Ok(Self)
+ }
+ }
impl Drop for Com {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
fn main() -> Result<()> {
- unsafe { CoInitialize(ptr::null())?; }
let _com = Com::new()?;
// omit
Ok(())
}
VARIANT 構造体
ヘルパー関数を用いて各タイプ(VT_I4やVT_BSTR)に対応したVARIANT構造体の初期化を行っていましたが、windows crate でimpl From<i32> for VARIANTやimpl From<&str> for VARIANTが実装されているのでそちらを使います。ヘルパー関数は削除しました。
src/main.rs
- unsafe fn variant_int(value: i32) -> Variant {
- let mut variant = VARIANT::default();
- (*variant.Anonymous.Anonymous).vt = VT_I4.0 as u16;
- (*variant.Anonymous.Anonymous).Anonymous.lVal = value;
- Variant(variant)
-}
-
-unsafe fn variant_bstr<S: AsRef<str>>(value: S) -> Variant {
- let mut variant = VARIANT::default();
- (*variant.Anonymous.Anonymous).vt = VT_BSTR.0 as u16;
- (*variant.Anonymous.Anonymous).Anonymous.bstrVal = ManuallyDrop::new(value.as_ref().into());
- Variant(variant)
-}
-
- let visible = variant_int(0);
- let param = variant_bstr("path_to_excel.xlsx");
+ let visible: VARIANT = 0.into();
+ let param: VARIANT = "path_to_excel.xlsx".into();
まとめ
以下、コピペ用サンプルコード
Cargo.toml
[package]
name = "excel_to_pdf"
version = "0.2.0"
edition = "2024"
[dependencies]
anyhow = "1.0"
[dependencies.windows]
version = "0.62"
features = [
"Win32_System_Com",
"Win32_System_Ole",
"Win32_System_Variant",
]
main.rs
use anyhow::{Context, Result, ensure};
use std::path::Path;
use windows::{
Win32::System::{
Com::{
CLSCTX_LOCAL_SERVER, CLSIDFromProgID, CoCreateInstance, CoInitialize, CoUninitialize,
DISPATCH_FLAGS, DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT,
DISPPARAMS, IDispatch,
},
Ole::DISPID_PROPERTYPUT,
Variant::{VARIANT, VT_DISPATCH},
},
core::{GUID, HSTRING, PCWSTR, w},
};
struct Com;
impl Com {
fn new() -> Result<Self> {
unsafe { CoInitialize(None).ok()? };
Ok(Self)
}
}
impl Drop for Com {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
struct Variant(VARIANT);
impl Variant {
fn dispval(&self) -> Result<&IDispatch> {
ensure!(self.0.vt().eq(&VT_DISPATCH));
unsafe {
(*self.0.Anonymous.Anonymous)
.Anonymous
.pdispVal
.as_ref()
.context("no disp val")
}
}
}
fn auto_wrap(
auto_type: DISPATCH_FLAGS,
pdisp: &IDispatch,
ptname: &str,
args: Vec<VARIANT>,
) -> Result<Variant> {
// Get DISPID for name passed...
let ptname: HSTRING = ptname.into();
let mut disp_id = 0;
unsafe {
pdisp.GetIDsOfNames(
&GUID::default(),
&PCWSTR::from_raw(ptname.as_ptr()),
1,
0x0400, // LOCALE_USER_DEFAULT 1024u32
&mut disp_id,
)?
};
// Build DISPPARAMS
let mut dp = DISPPARAMS {
cArgs: args.len() as _,
rgvarg: args.as_ptr() as _,
..Default::default()
};
// Handle special-case for property-puts!
if (auto_type & DISPATCH_PROPERTYPUT).ne(&DISPATCH_FLAGS(0)) {
dp.cNamedArgs = 1;
dp.rgdispidNamedArgs = &DISPID_PROPERTYPUT as *const _ as _;
}
let mut vresult = VARIANT::default();
// Make the call!
unsafe {
pdisp.Invoke(
disp_id,
&GUID::default(),
0x0800,
auto_type,
&dp,
Some(&mut vresult),
None,
None,
)?
};
Ok(Variant(vresult))
}
fn excel_to_pdf(path: &str) -> Result<()> {
// Initialize COM for this thread...
let _com = Com::new()?;
// Get CLSID for our server...
let cls_id = unsafe { CLSIDFromProgID(w!("Excel.Application")) }?;
// Start server and get IDispatch...
let pxlapp: IDispatch = unsafe { CoCreateInstance(&cls_id, None, CLSCTX_LOCAL_SERVER) }?;
// Make it visible (i.e. app.visible = 1)
let visible: VARIANT = 0.into();
auto_wrap(DISPATCH_PROPERTYPUT, &pxlapp, "Visible", vec![visible])?;
// Suppress alerts
let display_alerts: VARIANT = 0.into();
auto_wrap(
DISPATCH_PROPERTYPUT,
&pxlapp,
"DisplayAlerts",
vec![display_alerts],
)?;
// Get Workbooks collection
let result = auto_wrap(DISPATCH_PROPERTYGET, &pxlapp, "Workbooks", vec![])?;
let pxlbooks = result.dispval()?;
// Call Workbooks.Open() to get a new workbook...
let param: VARIANT = path.into();
let result = auto_wrap(DISPATCH_PROPERTYGET, pxlbooks, "Open", vec![param])?;
let pxlbook = result.dispval()?;
// Get ActiveSheet object
let result = auto_wrap(DISPATCH_PROPERTYGET, pxlbook, "ActiveSheet", vec![])?;
let pxlsheet = result.dispval()?;
// Call wb.ActiveSheet.ExportAsFixedFormat(xlTypePDF, "path to pdf")
let typ: VARIANT = 0.into();
let path = Path::new(path);
let parent = path.parent().context("no parent")?;
let file_stem = path.file_stem().context("no file stem")?;
let pdf_path = parent.join(format!("{}.pdf", file_stem.to_str().context("no str")?));
let pdf_path: VARIANT = pdf_path.to_str().context("no pdf path")?.into();
auto_wrap(
DISPATCH_METHOD,
pxlsheet,
"ExportAsFixedFormat",
vec![pdf_path, typ],
)?;
// wb.Close(False)
let close: VARIANT = false.into();
auto_wrap(DISPATCH_METHOD, pxlbook, "Close", vec![close])?;
// Tell Excel to quit (i.e. App.Quit)
auto_wrap(DISPATCH_METHOD, &pxlapp, "Quit", vec![])?;
Ok(())
}
fn main() -> Result<()> {
excel_to_pdf("C:\\path\\to\\Book1.xlsx")?;
Ok(())
}