fish-dock-plugin-filemgr 0.0.0-stage → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # 文件管理
2
+
3
+ 把 FISH Dock 变成随手可用的文件工作台:左边逛目录、右边看内容,中间还能把常用文件先「暂存」起来,随时拖进别的程序。
4
+
5
+ ## 功能
6
+
7
+ ### 目录浏览 + 预览
8
+
9
+ - **目录浏览**:从磁盘根目录、用户目录、桌面 / 文档 / 下载逐层进入;支持把文件 / 文件夹直接拖进面板。
10
+ - **预览**:Markdown 渲染、图片(PNG / JPEG / GIF / WebP / BMP / AVIF / ICO / SVG)、Office 文本抽取(docx 段落与标题、xlsx 多工作表、pptx 每页文字,原生解析 OOXML,不需要装 Office)、代码高亮行号、纯文本与二进制兜底。
11
+ - **上下并列**:上半是文件夹浏览器(面包屑 + 列表 + 上一级 / 刷新 / 常用文件夹 / 固定),下半是预览;中间分隔条可拖动调高,高度会被记住。
12
+ - **框选与拖入暂存**:在列表里拖出选框可批量选中(Ctrl / Shift 点选切换),选中项直接拖到左侧暂存区即批量加入;拖到一半拖出列表会自动从「框选」切到「搬运」。
13
+ - **常用文件夹**:记住打开过的目录,浮层里快速跳转;可把目录「固定」到最上面。
14
+
15
+ ### 选择文件夹
16
+
17
+ 「打开文件夹」默认调用 **Windows 原生文件夹选择框**(`pick-folder.ps1` + IFileOpenDialog),选完直接进入该目录。
18
+
19
+ 宿主原本会在系统对话框抢走焦点时自动收起设置面板。插件只在**弹出对话框的那一刻**调用 `bridge.js` 的 `hold(true)`,选完(或取消/报错)立刻在 `finally` 里 `hold(false)` 放开——平时浏览、拖放都不锁,点面板外面照常能关。
20
+
21
+ 宿主没有 `fish:hold` 能力(官方发行版 ≤ v1.2.3)时,插件自动回落到**内置自绘选择器**(用后端 `preview.tree` 逐层浏览),功能完整,只是不锁面板:
22
+
23
+ | 宿主 | 「打开文件夹」 | 面板锁定 |
24
+ |---|---|---|
25
+ | 自行编译、带 `fish:hold` 的宿主 | Windows 原生对话框 | 仅对话框期间临时锁定 |
26
+ | 官方发行版(≤ v1.2.3) | 内置选择器 | 不锁定 |
27
+
28
+ 宿主侧要支持这条路,需要三处改动(都已在 `FISH-Dock` 上游以补丁形式验证过):
29
+
30
+ 1. `sdk/browser/bridge.js` 暴露 `hold(on)` → `parent.postMessage({type:'fish:hold', ticket, on}, '*')`
31
+ 2. `Wakuwaku/src/pet/extension-page.js` 收 `fish:hold` 写入 `record.hold`,并新增 `onHold(record, on)` 回调;`park()` / `enter()` / `retire()` 各自同步
32
+ 3. `Wakuwaku/src/pet/settings.js` 维护 `pluginHold` 标志,`onBlur` 判定里加 `!pluginHold`
33
+
34
+ 只改插件包、不重编译宿主时,插件自动走内置选择器,功能完整不缺。
35
+
36
+ ### 文件暂存(中转站)
37
+
38
+ - 拖入文件或文件夹即加入暂存区,也可以右键 / 按钮添加。
39
+ - **分组**:暂存区支持多个分组标签,点击切换,标签可折叠;删除分组只走标签右侧的 ×。
40
+ - **拖出**:把暂存项直接拖到桌面、资源管理器、微信等外部程序。
41
+ - 复制路径、复制为文件、多选批量、去重、清空,偏好持久化。
42
+
43
+ ## 说明
44
+
45
+ - 只读取路径白名单内的文件,路径解析后必须落在允许的根目录之下;UNC 路径一律拒绝。
46
+ - 「打开文件夹」在支持 `fish:hold` 的宿主上会短暂启动系统 PowerShell 调起 Windows 原生文件夹对话框(`pick-folder.ps1`),用于拿到用户亲手选择的路径;不加 `fish:hold` 的宿主上不会启动子进程,走内置选择器。除此之外不启动任何子进程。
47
+ - 页面运行在宿主沙箱里(`default-src 'none'`),PDF 与音视频不做内联播放,会提示用系统程序打开。
48
+ - 不发起网络请求。
49
+
50
+ ## 存储
51
+
52
+ 偏好(布局、最近打开、额外根目录、暂存分组)保存在插件命名空间的 `config/prefs.json`。
53
+
54
+ ## 开发
55
+
56
+ ```bash
57
+ # 本地暂存(模拟官方 tools/stage-development.mjs 的产出)
58
+ node _stage_filemgr.mjs
59
+ # 产物
60
+ # dist/filemgr.fish-plugin
61
+ # _stage/filemgr/ ← 可直接用 FISH Dock 的「从文件安装」装上验证
62
+ ```
package/bridge.js ADDED
@@ -0,0 +1,53 @@
1
+ // 插件浏览器 SDK:只连接父页面,所有业务调用由票据锁定当前插件。
2
+ ;(function () {
3
+ const ticket = location.pathname.split('/')[1]
4
+ let sequence = 0
5
+ const pending = new Map()
6
+ window.addEventListener('message', event => {
7
+ const row = event.data
8
+ if (event.source !== parent || !row || row.ticket !== ticket) return
9
+ if (row.type === 'fish:theme') {
10
+ const palette = row.colors || {}
11
+ for (const [name, value] of Object.entries(palette)) if (/^--dock-[a-z-]+$/.test(name) && typeof value === 'string' && /^(#[a-f\d]{3,8}|rgba?\([\d.,%\s]+\))$/i.test(value)) document.documentElement.style.setProperty(name, value)
12
+ document.documentElement.dataset.dockTheme = row.mode === 'light' ? 'light' : 'dark'
13
+ const title = document.getElementById('title')
14
+ if (title && typeof row.title === 'string') title.textContent = row.title
15
+ return
16
+ }
17
+ if (row.type !== 'fish:result') return
18
+ const waiter = pending.get(row.id)
19
+ if (!waiter) return
20
+ pending.delete(row.id); clearTimeout(waiter.timer)
21
+ if (row.error) waiter.reject(new Error(row.error)); else waiter.resolve(row.result)
22
+ })
23
+ function call(method, params = {}) {
24
+ if (pending.size >= 32) return Promise.reject(new Error('插件页面请求过多,请稍后重试'))
25
+ const id = ++sequence
26
+ return new Promise((resolve, reject) => {
27
+ const timer = setTimeout(() => { pending.delete(id); reject(new Error('插件页面请求超时')) }, 20000)
28
+ pending.set(id, { resolve, reject, timer })
29
+ parent.postMessage({ type: 'fish:call', ticket, id, method, params }, '*')
30
+ })
31
+ }
32
+ let layout = 'single'
33
+ function reportLayout() {
34
+ const main = document.querySelector('main') || document.body
35
+ if (!main) return
36
+ const height = Math.ceil(main.getBoundingClientRect().height)
37
+ if (height > 0) parent.postMessage({ type:'fish:layout', ticket, height, layout }, '*')
38
+ }
39
+ function setLayout(value) {
40
+ if (!['single','double'].includes(value)) return
41
+ layout = value
42
+ requestAnimationFrame(reportLayout)
43
+ }
44
+ function observeLayout() {
45
+ parent.postMessage({ type: 'fish:ready', ticket }, '*')
46
+ const main = document.querySelector('main') || document.body
47
+ if (!main || typeof ResizeObserver === 'undefined') return
48
+ new ResizeObserver(reportLayout).observe(main)
49
+ }
50
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', observeLayout, { once: true }); else observeLayout()
51
+ parent.postMessage({ type: 'fish:ready', ticket }, '*')
52
+ Object.defineProperty(window, 'FishDock', { value: Object.freeze({ call, setLayout }), writable: false, configurable: false })
53
+ })()
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "fish-dock-plugin-filemgr",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.5",
4
+ "description": "FISH Dock 文件管理插件:目录浏览 + 文件预览(Markdown / 图片 / Office / 代码)+ 文件暂存中转站,兼容 FISH Dock v1.2.3 及更早官方发行版。",
5
+ "type": "module",
6
+ "files": [
7
+ "plugin.json",
8
+ "README.md",
9
+ "bridge.js",
10
+ "plugins"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "keywords": [
16
+ "fish-dock",
17
+ "fishdock",
18
+ "plugin",
19
+ "file-manager",
20
+ "file-preview",
21
+ "markdown",
22
+ "stash",
23
+ "windows"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/jassdingk/fish-dock-plugin-filemgr.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/jassdingk/fish-dock-plugin-filemgr/issues"
31
+ },
32
+ "homepage": "https://github.com/jassdingk/fish-dock-plugin-filemgr#readme",
33
+ "author": "jassdingk",
34
+ "license": "MIT",
35
+ "os": [
36
+ "win32"
37
+ ]
38
+ }
package/plugin.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "schema": 1,
3
+ "id": "jassdingk.filemgr",
4
+ "name": "文件管理",
5
+ "version": "1.0.5",
6
+ "hostApi": 1,
7
+ "platforms": [
8
+ "windows-x86_64"
9
+ ],
10
+ "storage": {
11
+ "directory": ".plugin.jassdingk.filemgr",
12
+ "dataSchema": 1
13
+ },
14
+ "backend": {
15
+ "runtime": "node",
16
+ "entry": "plugins/filemgr/backend.mjs"
17
+ },
18
+ "capabilities": [
19
+ "storage"
20
+ ],
21
+ "pages": [
22
+ {
23
+ "id": "main",
24
+ "title": "文件管理",
25
+ "entry": "plugins/filemgr/page.html"
26
+ }
27
+ ],
28
+ "files": {
29
+ "bridge.js": "f38f93994e3cb2b71f2ff1810b3f68fb42f4261550f8ab01b0bfa3eecd5f94ee",
30
+ "package.json": "7e9124508cc113f5039ce514573a5f8f280faf9392e4677d30bb144c55351e5c",
31
+ "plugins/filemgr/backend.mjs": "b448aa688ff11f2abc4c5281b4714646ed6c384473d873cfe431a8e64b066d26",
32
+ "plugins/filemgr/clip.ps1": "b0a32fca35cf2d25133338f46e20a0db20d78c5bb6b5c2aab899998bfbd6c6ab",
33
+ "plugins/filemgr/dock.css": "e795214dd39de3b473ac0b1a7b79f6922a88c761fca4d3ea4da8fd9b60188bcf",
34
+ "plugins/filemgr/filemgr.css": "16c621471133d29c6942781bedf86e0a4de806865ff004b666bcad8b1929434e",
35
+ "plugins/filemgr/FolderPicker.ps1": "4b7f86c543e06fd8f93fa5d8002997d944c8189cb0ae031bedf864ea89196427",
36
+ "plugins/filemgr/page.css": "0801a05b5148cd091a68957beac21cc982a7818ef4707cd06fd11b20adbc7765",
37
+ "plugins/filemgr/page.html": "3cfda31be29cd23b1e0f7f6004b6c03522c3526b78f9df4711e320962a660831",
38
+ "plugins/filemgr/page.js": "282f11d803ddc8bb87a0a99d8af68315f4ff7304672072da838a07a47c5cdc62",
39
+ "plugins/filemgr/pick-files.ps1": "88bff92d9a2e5b69e9411bb372441ff0b86899b7f5b31f5ed8d154c4f6fc2f37",
40
+ "plugins/filemgr/pick-folder.ps1": "ff031c14df32149c1c80c7ab6a21710c1a68e823e96e3068c458a60902ae3b6f",
41
+ "plugins/filemgr/preview.mjs": "a5596e77180fe7928443244a0148b6d3ea8381a1f886ee01f9a08be038c7ee80",
42
+ "plugins/filemgr/sdk.mjs": "76b052b8d99ff6b6b0b149aee7479d1129bb248a959625642282184978065f38",
43
+ "plugins/filemgr/stash.css": "fce2aaa13a86dbd7675b685c533f0bb916c7e610fc612a3ade660c67069f5662",
44
+ "plugins/filemgr/stash.mjs": "c33cea6b845a3847950d26a10b9aba1ef68d39b1f9fc423b23055dead7638866",
45
+ "README.md": "e9fb4ca9831b778a1d530b67518fcaf2b42cf02494b9d68deaad16bf2b947687"
46
+ }
47
+ }
@@ -0,0 +1,260 @@
1
+ # Windows 原生文件夹/文件选择对话框的 C# 实现(IFileOpenDialog)。
2
+ # 由 pick-folder.ps1 / pick-files.ps1 通过 Add-Type -TypeDefinition 编译,避免依赖 .cs 文件。
3
+ $FolderPickerSource = @'
4
+ // Windows 原生"选择文件夹"对话框(IFileOpenDialog + FOS_PICKFOLDERS)。
5
+ // 与 FISH Dock 宿主 paper.rs:588 用的是同一个 COM 接口,所以外观和系统资源管理器
6
+ // 的"选择文件夹"完全一致,而不是 WinForms 那棵 1995 年的老树。
7
+ using System;
8
+ using System.Runtime.InteropServices;
9
+
10
+ namespace FishDockPick
11
+ {
12
+ [ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"),
13
+ InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
14
+ internal interface IFileDialog
15
+ {
16
+ [PreserveSig] int Show(IntPtr parent);
17
+ void SetFileTypes(uint c, IntPtr rg);
18
+ void SetFileTypeIndex(uint i);
19
+ void GetFileTypeIndex(out uint i);
20
+ void Advise(IntPtr e, out uint c);
21
+ void Unadvise(uint c);
22
+ void SetOptions(uint o);
23
+ void GetOptions(out uint o);
24
+ void SetDefaultFolder(IShellItem si);
25
+ void SetFolder(IShellItem si);
26
+ void GetFolder(out IShellItem si);
27
+ void GetCurrentSelection(out IShellItem si);
28
+ void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string name);
29
+ void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string name);
30
+ void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string title);
31
+ void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string text);
32
+ void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string label);
33
+ void GetResult(out IShellItem si);
34
+ void AddPlace(IShellItem si, int fdap);
35
+ void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string ext);
36
+ void Close(int hr);
37
+ void SetClientGuid(ref Guid guid);
38
+ void ClearClientData();
39
+ void SetFilter(IntPtr filter);
40
+ }
41
+
42
+ [ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"),
43
+ InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
44
+ internal interface IShellItem
45
+ {
46
+ void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
47
+ void GetParent(out IShellItem parent);
48
+ void GetDisplayName(uint sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string name);
49
+ void GetAttributes(uint sfgaoMask, out uint sfgaoAttribs);
50
+ void Compare(IShellItem psi, uint hint, out int order);
51
+ }
52
+
53
+ [ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
54
+ internal class FileOpenDialog { }
55
+
56
+ // IFileOpenDialog — 多选文件需要这个接口(继承 IFileDialog,多了 GetResults)。
57
+ [ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"),
58
+ InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
59
+ internal interface IFileOpenDialog
60
+ {
61
+ [PreserveSig] int Show(IntPtr parent);
62
+ void SetFileTypes(uint c, IntPtr rg);
63
+ void SetFileTypeIndex(uint i);
64
+ void GetFileTypeIndex(out uint i);
65
+ void Advise(IntPtr e, out uint c);
66
+ void Unadvise(uint c);
67
+ void SetOptions(uint o);
68
+ void GetOptions(out uint o);
69
+ void SetDefaultFolder(IShellItem si);
70
+ void SetFolder(IShellItem si);
71
+ void GetFolder(out IShellItem si);
72
+ void GetCurrentSelection(out IShellItem si);
73
+ void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string name);
74
+ void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string name);
75
+ void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string title);
76
+ void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string text);
77
+ void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string label);
78
+ void GetResult(out IShellItem si);
79
+ void AddPlace(IShellItem si, int fdap);
80
+ void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string ext);
81
+ void Close(int hr);
82
+ void SetClientGuid(ref Guid guid);
83
+ void ClearClientData();
84
+ void SetFilter(IntPtr filter);
85
+ void GetResults(out IShellItemArray ppenum);
86
+ void GetSelectedItems(out IShellItemArray ppsai);
87
+ }
88
+
89
+ [ComImport, Guid("b63ea76d-1f85-456f-a19c-48159efa858b"),
90
+ InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
91
+ internal interface IShellItemArray
92
+ {
93
+ void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
94
+ void GetPropertyStore(int flags, ref Guid riid, out IntPtr ppv);
95
+ void GetPropertyDescriptionList(IntPtr keyType, ref Guid riid, out IntPtr ppv);
96
+ void GetAttributes(uint AttribFlags, uint sfgaoMask, out uint psfgaoAttribs);
97
+ void GetCount(out uint pdwNumItems);
98
+ void GetItemAt(uint dwIndex, out IShellItem ppsi);
99
+ void EnumItems(out IntPtr ppenumShellItems);
100
+ }
101
+
102
+ public static class FolderPicker
103
+ {
104
+ private const uint FOS_PICKFOLDERS = 0x20;
105
+ private const uint FOS_FORCEFILESYSTEM = 0x40;
106
+ private const uint FOS_PATHMUSTEXIST = 0x800;
107
+ private const uint FOS_ALLOWMULTISELECT = 0x200;
108
+ private const uint SIGDN_FILESYSPATH = 0x80058000;
109
+ private const int ERROR_CANCELLED = unchecked((int)0x800704C7);
110
+
111
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
112
+ private static extern void SHCreateItemFromParsingName(
113
+ [MarshalAs(UnmanagedType.LPWStr)] string pszPath,
114
+ IntPtr pbc,
115
+ ref Guid riid,
116
+ [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv);
117
+
118
+ [DllImport("ole32.dll")]
119
+ private static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);
120
+
121
+ [DllImport("ole32.dll")]
122
+ private static extern void CoUninitialize();
123
+
124
+ [DllImport("user32.dll")]
125
+ private static extern IntPtr GetForegroundWindow();
126
+
127
+ [DllImport("user32.dll")]
128
+ private static extern bool SetForegroundWindow(IntPtr hWnd);
129
+
130
+ [DllImport("user32.dll")]
131
+ private static extern bool AllowSetForegroundWindow(int dwProcessId);
132
+
133
+ // 只初始化 COM 和创建对话框对象,不弹窗——供自检用。
134
+ public static string Selftest()
135
+ {
136
+ int hr = CoInitializeEx(IntPtr.Zero, 2 /* APARTMENTTHREADED */);
137
+ try
138
+ {
139
+ IFileDialog dlg = (IFileDialog)new FileOpenDialog();
140
+ uint opts;
141
+ dlg.GetOptions(out opts);
142
+ dlg.SetOptions(opts | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
143
+ dlg.SetTitle("选择文件夹");
144
+ return "COM_OK hr=0x" + hr.ToString("X8");
145
+ }
146
+ finally
147
+ {
148
+ if (hr >= 0) CoUninitialize();
149
+ }
150
+ }
151
+
152
+ public static string Pick(string initial, string title)
153
+ {
154
+ int hr = CoInitializeEx(IntPtr.Zero, 2 /* APARTMENTTHREADED */);
155
+ try
156
+ {
157
+ IFileDialog dlg = (IFileDialog)new FileOpenDialog();
158
+ uint opts;
159
+ dlg.GetOptions(out opts);
160
+ dlg.SetOptions(opts | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
161
+
162
+ if (!string.IsNullOrEmpty(title)) dlg.SetTitle(title);
163
+
164
+ if (!string.IsNullOrEmpty(initial))
165
+ {
166
+ try
167
+ {
168
+ Guid iid = new Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe");
169
+ IShellItem folder;
170
+ SHCreateItemFromParsingName(initial, IntPtr.Zero, ref iid, out folder);
171
+ if (folder != null) dlg.SetFolder(folder);
172
+ }
173
+ catch { /* 起始目录不存在就退回系统默认 */ }
174
+ }
175
+
176
+ // 关键:本进程是被宿主当子进程拉起来的、且宿主页面还是隐藏窗口,
177
+ // 系统默认会把对话框排在后面,用户看到的是"点了没反应"。
178
+ // 先让操作系统允许本进程抢前台,再 Show 时把父窗口设为 NULL,
179
+ // 弹出来之后主动 SetForegroundWindow,保证一定浮在最上面。
180
+ AllowSetForegroundWindow(-1 /* ASFW_ANY */);
181
+
182
+ int show = dlg.Show(IntPtr.Zero);
183
+ if (show == ERROR_CANCELLED || show != 0) return null;
184
+
185
+ IShellItem item;
186
+ dlg.GetResult(out item);
187
+ if (item == null) return null;
188
+
189
+ string path;
190
+ item.GetDisplayName(SIGDN_FILESYSPATH, out path);
191
+ return path;
192
+ }
193
+ finally
194
+ {
195
+ if (hr >= 0) CoUninitialize();
196
+ }
197
+ }
198
+
199
+ /// <summary>
200
+ /// 弹出多选文件对话框(无 FOS_PICKFOLDERS,加 FOS_ALLOWMULTISELECT)。
201
+ /// 返回选中的路径数组,取消返回空数组。
202
+ /// </summary>
203
+ public static string[] PickFiles(string initial, string title)
204
+ {
205
+ int hr = CoInitializeEx(IntPtr.Zero, 2 /* APARTMENTTHREADED */);
206
+ try
207
+ {
208
+ var dlg = (IFileOpenDialog)new FileOpenDialog();
209
+ uint opts;
210
+ dlg.GetOptions(out opts);
211
+ dlg.SetOptions(opts | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_ALLOWMULTISELECT);
212
+
213
+ if (!string.IsNullOrEmpty(title)) dlg.SetTitle(title);
214
+
215
+ if (!string.IsNullOrEmpty(initial))
216
+ {
217
+ try
218
+ {
219
+ Guid iid = new Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe");
220
+ IShellItem folder;
221
+ SHCreateItemFromParsingName(initial, IntPtr.Zero, ref iid, out folder);
222
+ if (folder != null) ((IFileDialog)dlg).SetFolder(folder);
223
+ }
224
+ catch { /* 起始目录不存在就退回系统默认 */ }
225
+ }
226
+
227
+ AllowSetForegroundWindow(-1 /* ASFW_ANY */);
228
+
229
+ int show = ((IFileDialog)dlg).Show(IntPtr.Zero);
230
+ if (show == ERROR_CANCELLED || show != 0) return new string[0];
231
+
232
+ IShellItemArray results;
233
+ dlg.GetResults(out results);
234
+ if (results == null) return new string[0];
235
+
236
+ uint count;
237
+ results.GetCount(out count);
238
+ var paths = new string[count];
239
+ for (uint i = 0; i < count; i++)
240
+ {
241
+ IShellItem item;
242
+ results.GetItemAt(i, out item);
243
+ if (item != null)
244
+ {
245
+ string path;
246
+ item.GetDisplayName(SIGDN_FILESYSPATH, out path);
247
+ paths[i] = path;
248
+ }
249
+ }
250
+ return paths;
251
+ }
252
+ finally
253
+ {
254
+ if (hr >= 0) CoUninitialize();
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ '@