我创建了一个使用Fluent UI React v9开发的应用程序,可以编辑Paint.NET的选择区域文本
首先
我为Paint.NET开发了一个用于编辑选定范围的应用程序。
-
- Paint.NET Selection Editor
- hossy3/paintdotnet-selection-editor: Selection Editor for Paint.NET
这篇文章是关于以下技术案例的介绍。这些是我们尝试模仿学习的。可能还有更好的方法。
-
- ロジック寄りの話
Paint.NET の「選択範囲自体をコピー」で得られる情報
Paint.NET の選択範囲を編集する
アプリケーション寄りの話
開発環境用意 (Node.js + VS Code + プラグイン)
TypeScript + React でアプリを作る (create-react-app)
gh-pages でデプロイする
Fluent UI React v9 を組み込む
Fluent UI React v9 の状態管理 (React.useReducer())
クリップボードにテキストをコピー・ペーストする
フォームで数値入力
Undo / Redo
選択範囲編集のロジック実装と単体テスト
React で Canvas 2D context を書き換える
canvas の再描画を減らす
我认为这种组合太符合个人需要了。如果能够在偷吃中对您有所帮助,我会感到幸福。
请提供一个例子
-
- 画面キャプチャーしたあと、ダイアログなどの特定の場所だけをぴったり切り取りたい。周辺のものができるだけ映りこんでほしくない
- 640 * 480 などの決まったサイズに切り取りたい
比如说,假设我们想要在Twitter上张贴AtCoder ABC300的结果。
当你以自由的手法选择并复制时,框线的外侧将被包含在图像中。使用鼠标或触摸面板精确选择角落是相当困难的。

使用Paint.NET的自动选择工具,快速选择上下左右的边缘部分,并使用该工具将其转换为矩形形状。

周围的反光消失了。
大致上,在粗略地矩形選擇後,您可以放大屏幕,使用「選擇區域移動」工具進行調整,這通常能夠解決問題。
我想要制作的原因
我会经常进行类似示例中的操作。在使用Corel PaintShop Pro时,我会使用Python脚本来进行操作。
我最近在使用的Paint.NET软件的选择范围编辑功能较弱,所以我在寻找能否通过插件来实现。在这个过程中,我发现了以下的帖子。
- ‘Callout Selection’ plugin development – Plugin Developer’s Central – paint.net Forum
作为标准功能,即使没有选择区域的编辑功能,也可以通过JSON 提取出来,所以可以使用脚本语言等自由编辑。我想,如果能写出一款使用了竞技编程类似逻辑的、有一定实用性的应用程序,那肯定有趣,所以我试着尝试了一下。
开发语言可以是任何一种都可以。选择了 TypeScript 是因为它可以直接使用而无需安装,并且似乎更容易在剪贴板上进行值的交换。
-
- microsoft/fluentui: Fluent UI web represents a collection of utilities, React components, and web components for building web applications.
- Fluent UI v9 がリリースされていました
顺便说一下,既然要用 TypeScript,就试试下一版的 Microsoft Teams 可能会使用的 Fluent UI React v9 ,我挺感兴趣的。
偏向于逻辑的讲述
使用Paint.NET的”复制选定区域本身”选项获得的信息。
例如,我们可以通过以下方式得到两个多边形的路径点序列。内外判断是基于多边形重叠数量的奇偶性进行的。两个重叠多边形内部的矩形被视为不属于选择范围的”孔”。
{
"polygonList": [
"367,331,363,331,363,326,367,326",
"369,324,361,324,361,334,371,334"
]
}
请用中文将以下内容转述为一个选项:
请提供翻译的具体句子。
[(367,331), (363,331), (363,326), (367,326)],
[(369,324), (361,324), (361,334), (371,334)]

- CanvasRenderingContext2D: fill() method – Web APIs | MDN
这是一个Canvas路径填充的奇偶规则图像。
多边形列表的定义没有在手册中写明。根据我查找的例子,它是这样的。
-
- 整数でなく実数が入ることもある
実数の場合はアンチエイリアスがかかる
実数計算での誤差らしい値が入っていることもあります
Paint.NET の選択範囲は 1つのループ内で交差しない
点接触はありえます
ループ方向 左回り・右回りは内外判定に関係しない 2
ループ出現順も内外判定に関係しない
ループの始終点に同じ座標が入っていても良いし、離れていても良い
ループ 1つの場合には始終点に同じ座標が入るらしいです
编辑Paint.NET的选取区域。
将其变成四边形.

找出所有点的 x 的最大和最小值,以及 y 的最大和最小值。(xmin, ymin) 到 (xmax, ymax) 的矩形边界即为所求的 Bounding Box。
清除穴道

- AtCoder ABC296-G – Polygon and Points
先月在 AtCoder ABC296-G 上出现了一个问题,需要快速检查一个点是否位于多边形内部。这个问题与此类似。
从循环的任意一个点上下拉直线,检查与其他循环的干涉数量。如果上下都是奇数,则为内部;否则为外部。
当循环数量很多时,组合数量会急剧增加。我们可以在每次循环中计算边界框,并按照边界框的大小顺序进行检查,如果某个边界框完全包含在其他边界框中,则进一步详细检查该内部可能性,从而减少计算量。
按照这个方式进行实现,然后编写单元测试。
偏重于应用的讨论
提供适用于开发环境的软件工具(Node.js + VS Code + 插件)
请将以下两个项目进行安装。
-
- Node.js
- Visual Studio Code – コード エディター | Microsoft Azure
另外还安装了以下三个 VS Code 插件。
-
- Jest – Visual Studio Marketplace
-
- ESLint – Visual Studio Marketplace
- Prettier – Code formatter – Visual Studio Marketplace
使用 TypeScript 和 React 创建应用程序(create-react-app)。
-
- 新しい React アプリを作る – React
- Adding TypeScript | Create React App
按照 create-react-app 的说明,创建一个新项目。使用 –template typescript 选项以启用 TypeScript。
npx create-react-app paintdotnet-selection-editor --template typescript
运行 npm run start 后,会在浏览器中显示页面。
npm run start

我会从这里开始制作应用程序。
使用gh-pages进行部署
我还没有做任何东西,但会确认能够发布应用程序。
-
- Deployment | Create React App
- GitHub Pages サイトを作成する – GitHub Docs
刚刚在 create-react-app 的说明页面中详细介绍了如何使用 GitHub 上的 gh-pages 进行部署。虽然有一些限制,但对于在公共存储库上开发的小型 WEB 应用程序来说,这种方法似乎已经足够了。
npm install --save gh-pages
"scripts": {
+ "predeploy": "npm run build",
+ "deploy": "gh-pages -d build",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
+ "homepage": "https://hossy3.github.io/paintdotnet-selection-editor"
在之后,可以选择一个合适的时机运行部署命令。这将在 gh-pages 分支上创建一个发布页面。
npm run deploy
将存储库设置为在Pages上发布gh-pages的设置。

到目前为止,已经发布了https://hossy3.github.io/paintdotnet-selection-editor。
如果在公共仓库中直接公开 gh-pages 的内容,那么是否存在别人将可疑代码放入 gh-pages 中的危险呢?我担心这种情况下网站可能不会得到更新,根据 GitHub 文档来看。
提示: 如果你正在尝试从分支发布,并且网站没有自动发布,请确保具有管理员访问权限和验证过的电子邮件地址的用户已将其推送到公开源。
听说使用GitHub Actions可以在主分支有更新时进行自动部署,但我还没有这样做。
将 Fluent UI React v9 集成进去
- Concepts / Developer / Quick Start – Page ⋅ Storybook
按照 Fluent UI React v9 的说明,安装该库。
npm install @fluentui/react-components
按照同一页上的指示,重写 index.tsx 和 App.tsx 文件。
import React from "react";
import ReactDOM from "react-dom";
import { FluentProvider, teamsLightTheme } from "@fluentui/react-components";
import App from "./App";
ReactDOM.render(
<FluentProvider theme={teamsLightTheme}>
<App />
</FluentProvider>,
document.getElementById("root")
);
import React from 'react';
import { Button } from '@fluentui/react-components';
function App() {
return (
<Button appearance="primary">Get started</Button>
);
}
通过这样做,使用 create-react-app 创建的页面将被一个按钮的页面所取代。
npm run start

打开浏览器的 DevTools 时,会显示一条警告信息。根据所知,这可能是 Fluent UI React v9 的已知问题,所以我决定等待修复。
警告:在React 18中已不再支持ReactDOM.render方法,请改用createRoot方法。在您切换到新的API之前,您的应用将像运行React 17一样运行。了解更多:https://reactjs.org/link/switch-to-createroot
放置UI组件
-
- Components / Toolbar – Default ⋅ Storybook
-
- Components / Textarea – Default ⋅ Storybook
- Components / Dialog – Default ⋅ Storybook
除了按钮以外,还可以使用许多UI组件。查看API并将组件粘贴到网页上。

在此时候,它只是个假象。但仅仅贴上了零件,就感觉它似乎有动力了。
- Fluent UI React v9 Component Roadmap · microsoft/fluentui Wiki
在2023年5月的时候,Fluent UI React v9的组件似乎还没有完全齐全,它们是按照完成的顺序提供的。即使提供了工具栏,但直接处理下拉菜单时还有一些使用上的问题。
在 Fluent UI React v9 中,我们可以使用 React.useReducer() 来进行状态管理。
当按下“穴除去”按钮时,我希望能够读取文本框中的内容并应用逻辑。然而,由于使用React,向UI组件询问状态并不自然。
要在外部使用文本框的信息,似乎有两种选择。
- Components / Textarea – Default ⋅ Storybook
-
- 我们让文本框自己控制状态的变化。通过onChange()事件通知使用者,使用者可以了解文本框的状态。
针对Textarea示例,兼容Uncontrolled使用方式。
优点:易于使用
缺点:不能从外部修改值,仅能使用初始值defaultValue。
使用者管理文本框的状态。同样地,通过onChange()事件通知使用者。文本框将直接显示使用者传递的value属性。
针对Textarea示例,兼容Controlled使用方式。
优点:可以从外部修改值
缺点:需要自行管理状态和验证状态的合法性。
由于有一种情况是想要将文本框中的内容替换为去除了空格的结果,因此选择了第2个选项。
- useReducer – React
当文本框的内容发生变化时,与之相关的其他状态(如“是否有空位”)也会随之变化。这些处理工作不希望与用户界面绑定在一起,因此我们将其交给了 reducer 处理。
const App = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
// :
return (
// :
<Textarea
onChange={(_, data) => {
dispatch({ type: "set_selection", payload: { text: data.value } });
}}
value={state.selectionText}
/>
// :
);
}
将文本复制并粘贴到剪贴板中。
-
- クリップボードとのやりとり – Mozilla | MDN
- クリップボード API – Web API | MDN
请将内容粘贴到文本框中,虽然复制粘贴已足够,但我还是为您准备了它。
过去使用document.execCommand(“copy”)等方法。现在似乎推荐使用剪贴板 API。
复制(写入剪贴板)
onClick={() => {
navigator.clipboard.writeText(state.selectionText);
}}
只需要传递先前的状态即可,使用navigator.clipboard.writeText()将文本信息写入剪贴板。
粘貼(從剪貼板讀取)
onClick={() => {
navigator.clipboard.readText().then((text) => {
dispatch({ type: "set_selection", payload: { text } });
});
}}
你可以使用navigator.clipboard.readText()方法查询剪贴板中的文本信息。根据异步返回的结果,在先前的调度中传递文本即可。
在最初从剪贴板读取信息时,该网站会弹出一个对话框询问浏览器的安全性。因为如果持续监视剪贴板,可能会有不好的事情发生。对于MS Edge浏览器,可以通过浏览器的“Cookie和站点权限”设置进行更改。

在表格中输入数字。

-
- Components / Dialog – Default ⋅ Storybook
- Components / SpinButton – Default ⋅ Storybook
虽然有很多贴上的组件,但可以以相同的方式处理。因为这次没有像选择范围那样多个状态相互关联,所以我尝试使用简单的 useState 而不是 useReducer。
export const BoxFormDialog = (props: BoxFormDialogProps) =>
props.open ? <BoxFormDialogImpl {...props} /> : null;
const BoxFormDialogImpl = (props: BoxFormDialogProps) => {
const [x, setX] = React.useState(
props.box != null ? Math.round(props.box[0]) : 0
);
// :
return (
// :
<SpinButton
value={x}
onChange={(_, data) => onChange(data, setX)}
/>
当对话框的打开状态发生改变时,如果要改变表单的值,除了像上面那样先将其从虚拟 DOM 中移除再重新初始化外,还可以使用下面的方法,即利用 useEffect。我不太清楚哪种方法更好。
export const BoxFormDialogImpl = (props: BoxFormDialogProps) => {
const [x, setX] = React.useState(
props.box != null ? Math.round(props.box[0]) : 0
);
React.useEffect(
() => setX(props.box != null ? Math.round(props.box[0]) : 0),
[props.box]
);
撤销/重做
当你在浏览器的文本框中进行文本编辑时,通常可以使用Undo/Redo。但是,如果从外部修改该文本,则会丢失Undo /Redo信息。要在TypeScript中实现类似于浏览器文本框中的Undo/Redo操作,这让人感到头痛。
这次我们简单地决定只能在按钮操作后重新开始,具体如下。
-
- 「Undo/Redo」以外のテキストボックスを変更するボタンを押したとき
「変更前」「変更後」の両方の状態をスナップショットとして覚える
前後のスナップショットが同じ選択範囲を指す場合は保存を省略する
Redo バッファーがもしあればクリアする
「Undo/Redo」ボタンを押したとき
スナップショットを移動する
テキストボックスを操作したとき
なにもしない (テキストボックスにお任せ)

在中文中,有一种方法是记住操作历史记录,而不是保持状态不变。这样可以减少记忆的量。代替的是,在重做时重新进行填空和计算。
我认为,不仅可以在历史记录中前后移动,还可以通过下拉菜单一次性确认多个,并且能够快速跳转,这将非常方便。从状态管理的角度来看,应该是可以实现相同的功能的。因为在 Fluent UI React v9 的工具栏中实现下拉菜单很麻烦,所以我没有去做。
选择范围编辑的逻辑实现和单元测试
只需要直接实现“偏向逻辑的谈话”就可以了。创建测试用例是麻烦的。
如果在VS Code中安装了Jest插件,单元测试通过的地方将会有一个勾号。您可以在开发过程中轻松进行检查。

用 React 修改 Canvas 2D 上下文
-
- キャンバス API – Web API | MDN
- CanvasRenderingContext2D: fill() method – Web APIs | MDN
为了能够在预览中选择范围,我们使用了

因此,我尝试在props变化时,只在useEffect中重新绘制
React.useEffect(() => {
const ctx = canvasRef.current?.getContext("2d");
ctx.clearRect(0, 0, width + 1, height + 1);
const region = new Path2D();
for (const polygon of props.polygonList) {
const i_max = polygon.length / 2;
region.moveTo(polygon[0], polygon[1]);
for (let i = 1; i < i_max; i++) {
region.lineTo(polygon[i * 2], polygon[i * 2 + 1]);
}
region.closePath();
}
ctx.fill(region, "evenodd");
ctx.stroke(region);
ctx.save();
}, [
canvasRef,
width,
height,
props.polygonList,
]);
props.polygonList 是一个方便用于canvas绘图的二维数组,可以由文本框组装。
减少画布的重新绘制。
如果保持这样的话,canvas 会频繁重新绘制。原因是 useEffect 的触发条件包括 props.polygonList 的存在。
例如,我们来考虑在文本框的末尾添加换行符的情况。从语义上讲,选定范围不会改变。应该能够使用相同的polygonList在reducer中进行组装。
然而,在JavaScript中,即使内部内容相同,只要指向的是不同的对象,它们将被视为不同的对象。重新构建后将产生不同的对象,并且会进行重新绘制。
- Expect · Jest
在reducer的世界中,我写了一个处理,当选择范围与之前相同时,不改变state.polygonList。就像Jest的.toEqual(value)一样。
export const reducer = (state: State, action: Action): State => {
// :
const polygonList = toPolygonList(selectionText);
if (polygonListEquals(polygonList, state.polygonList)) {
return {
...state,
selectionText,
};
}
// :
return {
...state,
selectionText,
polygonList,
};
然后在组件的构建函数中进行记忆化。这样,即使在 reducer 中应用的状态发生变化,canvas 部分也只有在 state.polygonList 发生变化时才会成为需要重新绘制的目标。
const previewCanvas = React.useMemo(
() => (
<PreviewCanvas polygonList={state.polygonList} />
),
[state.polygonList]
);
做起来有点棘手的感觉。当我试图将React的函数式世界和canvas的过程式世界连接在一起时,就变成了这样的话题。
- Konva – JavaScript 2d canvas library
也许把工作交给库文件而非自行处理会更好。
最后
我在编辑选定范围时所需要的功能基本上都已经具备了。我已经使用了大约一个月。顺便说一下,我也有机会接触到了之前一直感兴趣但没有机会使用的技术,比如Fluent UI React v9,这真是太好了。
這個應用程式需要在 Paint.NET 和其之間來回切換,使用工具欄操作相當麻煩。也許如果只需要進行矩形選取的話,只使用瀏覽器貼上圖片進行處理就足夠了。這是下一次要做的事情。
到此为止,我们结束了“作ってみた”这一系列的文章。
与ABC296-G完全不同。因为无法确定循环的方向,输入也不一定是凸多边形,所以约束条件放松了一点,变得稍微困难一些。但是由于不需要考虑巨大坐标和接触性,我认为这个竞编问题相对容易。
Fluent UI React v9将在Microsoft 365中使用,我认为将来将变得更易用。从当前的角度来看,我觉得还需要再等待一段时间。