今日开发工作备忘录

一、客户端 1、使用的Tabulator设定了高度,具体高度是electron的应用窗口高度,减去固定值80,这样就可以令Tabulator尽可能充满application client的视野。减去的80是我页面中的“页面标题”高度。这个做法虽然不完美,但是应付下周的客户demo验收是足够了; 2、销售人员上传模板数据之后,增加了预处理检查,必填项目必须填写,如果没有填写,则给出提示信息、Tabulator行变成红色、提交按钮变成灰色不可用的; 3、引入了axios模块,并且在登录页面进行了一个简单的、假的axios请求,将用户登录后的基本信息请求回来,存储在main.js主进程中; 4、各个页面的渲染进程可以通过ipcMain去问主进程要数据,主进程通过response将需要的数据返回给渲染进程。现在主要就是页面要向主进程问用户登录信息; 5、上传数据(还是第2点)中除了必填项检查外,对于“创建人”和“维护人”两个数据列,如果填写了就是用填写的,如果没有填写就是用accountInfo、也就是登录时服务器回传回来的用户信息; 二、服务端 1、下载CodeIgniter4做为服务段框架; 2、升级PHP从8.1到8.3以令ci4能够启动运行; 3、ci4中写了一个最简单的api/login接口,没有任何实际功能,只是返回一个json数据; 三、接下来要完成的工作是: 1、上传数据接口的假实现; 2、拉取数据接口的假实现; 3、将client端的所有文件组织结构进行调整,按目录存储; 4、将项目打包上传到云端做每日备份。

Electron.js 30中使用Tabulator

最近在忙一个新的项目,因为项目工期要求很短、并且我对这个项目十分不想投入过多的精力,所以选择使用Electron.JS框架进行开发。 但是Electron默认是不支持ESM语法的,这就使得如果想在项目中引入Tabulator等依赖项目时,会遇到语法不兼容问题。 一、网上看到的介绍是错误的: 与网上说的在package.json中增加type: module不同,不需要在package.json中增加type声明。因为package.json中增加声明,似乎是告知electron框架,主进程使用ESM语法。但是实际上主进程、也就是main.js文件仍然是CommonJS语法的,无需支持ESM语法。 二、实际只需要在渲染层引入ESM语法的支持: 也就是在需要的html文件中,将引入script文件的地方进行调整,从: <script src=”a.js”></script> 改成: <script type=”module” src=”a.mjs”></script> 这样渲染层的js解析器、也就是chromium浏览器的V8引擎就知道引入的js脚本是支持ESM的,并且启用ESM语法支持了。 三、在a.mjs文件中: 此时在a.mjs文件中,不必再使用require引入包,可以使用import引入包了。 但是因为Tabulator并没有默认的default导出入口(我也不知道具体应该怎么更准确的描述)、同时不清楚为什么不能直接按照包名直接导入(虽然我也是NPM安装的),所以导入包的具体语句如下: import {TabulatorFull as Tabulator} from ‘./node_modules/tabulator-tables/dist/js/tabulator_esm.min.js’; 剩下的具体使用就和其他正常使用没有差别了。 四、打包: 比较担心这样“自己胡乱导入”的包,最终打包生成可执行资源包的时候是否会出问题,经过实际npm run…

Electron开发备忘

接了一个甲方的开发任务,经过选型考虑(其实是完全没有考虑),决定使用Electron进行开发。为什么要用Electron进行这次项目的开发呢?我对这个脚手架以及nodejs语言,几乎可以说是没有任何经验的。 主要考虑如下: 1、我想最终交付给用户的时候,是一个本地客户端的产品形式; 2、通过html快速实现页面、美观的页面; 3、这个产品本身就是一个BS架构的,那么在客户端这边,能够通过浏览器实现是最好的选择; 4、本来可以完全做成基于浏览器的形式,但是那会导致历史上经常遇到的问题:我无法控制用户的浏览器,尤其是一些“领导”们使用的浏览器,往往都是很陈旧的,页面表现会不一致。 暂且处于如上考虑,今天使用Electron搭建了一个基本开发环境,并且做了一些粗糙的页面,响应了页面上的事件,引入BootStrap5对页面进行美化,尝试了将项目make成exe运行包。感觉一切都可以接受。 另:当前开发环境在软件运行时,可以从login页面的底部看到。 一、项目存放路径: ~/Desktop/electron-app/my-electron-app 二、启动项目命令: npm run start 三、打包项目命令: npm run make 四、打包时为什么没有将bootstrap5的样式打入运行时的包中? 运行提示:filed to load resource net err_file not found…

看似简单的输入控件,工作量巨大

最近正在撰写、开发的小工具,我决定所有的控件都自己实现,例如最简单的“文本输入框”,看似简单,其实内部机制很多,开发起来工作量是巨大的。 这个控件不仅要完成文字的录入,还要有“焦点”和“光标”的概念,当然要有,如果没有焦点,APP又如何知道当前是否处于输入状态、输入内容要交给页面上的哪一个控件处理呢? 光标是“焦点”的外在表现形式,当前页面上的“可聚焦”控件,谁处于“焦点”状态,谁就是拥有闪烁光标的。并且光标还要可以左移、右移、移至行首、移至行尾、上翻页、下翻页,甚至还可以跨单词移动……这些对光标的控制就已经是一个小的子功能模块了。 只有焦点和光标,只能说是完成了基本的文字录入功能,但还欠缺对“中文输入法”这样的IME输入的支持。和ascii输入不同,IME输入是有两套输入文字的,一套是预输入文字、一套是正选提交文字,两套文字要同时显示在输入框中,并且预选输入文字是会实时变化、并且可能出现在正式提交文字的任何中间区域的。这就给渲染文字带来了更复杂的逻辑,想正确实现也不是几行、几十行代码就能实现的。 简单和鼠标的控制也要考虑到,因为输入框内的文字是可以通过键盘或鼠标进行“框选”的,框选部分文字会“反色”以示意为选中状态。 单纯的“选中”是不够的,还要支持对选中的文字进行复制、剪贴,或者对光标所处位置进行文字的粘贴操作。这就又涉及到对系统粘贴板的操作。 有了复制、粘贴等功能,自然而然想到的就是这个输入框实际是有上下文菜单的,鼠标点击右键或通过键盘操作,可以唤出上下文菜单,并通过菜单进行撤销、全选、复制、粘贴、剪贴等操作。 除了上面这些功能之外,输入框内的输入内容如果是网址,可能还要做蓝色渲染和鼠标可直接点击打开等操作,这就更细化,更繁琐了。 如上,像完成一个基本的input text control,看似简单,内部机制非常繁杂。

今天又对键帽模型进行了些许完善

前些年“冒失的创业”,想做一款自己的计算机键盘出来。结果发现“万事开头难”这句话对创业并不试用,创业是开头容易挺进难。当然也可能我连“开头”都没有开呢,只是一直在门外转悠吧。 做键盘的时候就连同着键帽也做了一些工作,做了一些键帽的模型文件。后来键盘的事情近乎于停滞了,也就没了后话。 前段时间闲来无事,将键帽模型放在了网上,结果还真有一些朋友需要。但是其中也有很多的问题和细节值得记录或再调整。问题主要是模型文件中的缺陷或值得继续完善的地方,例如今天我就又对模型进行了一些调整,搞了将近8个小时才做完。 值得记录的细节呢,则是一些朋友问的关于键帽的事情。有趣闻、也有常识、还有一些是设计或制造方面的话题,有一些有共性的问题,我觉得值得写上一笔或录个视频说上一说。这里便是一个文字稿的备忘,等到文字整理的差不多了,我就开始做一个有关键帽模型设计方面的小视频,这样今后再有别的朋友问,就无需重复解答,直接发个链接给对方,想来应该是比较省事的做法。 未完待续

自己动手写个小应用

一、放弃wxWidgets 之前尝试用wxWidgets框架写应用,后来越写越不顺手,所以当代码到达3000行的时候,我就动摇了。想了2-3天,不断地摇摆,最终决定放弃使用wxWidgets,自己实现。而且不借助已有的GUI框架,自己实现用到的UI组件。 因为我想实现的应用程序本身并不复杂,用到的UI组件也不会很多,所以实现难度估计应该不大。实际上经过这几天的初步尝试,感觉也还是非常良好的。 自己实现组件,主要就是对渲染和交互的控制,而且我只需要不到10个基础组件,所以现在代码量2000行左右,已经有了一个大概的模样,预计再有一周左右就可以粗糙的实现出来了。 自己动手实现这些组件更大的好处是控制权限更高,例如事件的传递机制,我可以按照自己的想法进行链式的、广播式的、或者就是定向的传递,十分的灵活。当然这种灵活的前提是建立在良好的封装的基础上的,否则一旦没有及时的进行归纳和封装,就有可能将代码写乱、写花,所以每一个新机能的增加,都要对所有已实现的部分进行一边重构、归纳,这是比较花时间的事情。 不过这样做也有好处,就是一旦当前的小工具实现完毕,就拥有了一个简陋的Simple GUI,这样可以为今后再实现其他软件进行复用,持续做下去,不仅能够丰富自己的基础代码库,也能令后期的开发越来越便捷。 如此看来,现在经历的“枯燥”也许是有价值的,只不过它需要时间去验证、还需要一定的坚持才能见到回报。 二、初步的实现和初步的想法 既然已经决定自己从底层一点点慢慢写起,我也就不急于将最终的工具实现出来了,至少不基于做出一个“公众版”出来,只要自己能先用起来也就可以了。 而且自己实现就意味着要在一张画布上自己实现每一个控件和事件,也就无所谓用什么引擎。基本上能用的窗口管理器+图像引擎都可以拿来使用。这样想来,OpenGL应该是首选、其次SDL2、再次DirectX12。既然无所谓,所以我索性就三种引擎都试一试,都写了一写,最终哪一套的实现效果好并且开发难度小,最终的“公众版本”就基于哪一套继续开发就好了。 至于这个工具的目标平台,其实最初我考虑就是做在Windows系统上,虽然当初选择wxWidgets也有一定的跨平台考虑,但并不迫切。可既然如今决定“在一张白纸上”实现自己的基础控件,也就是说理论上它的跨平台移植能力应该也是有的,所以等到做公众版的时候,可以考虑多平台一起实现出来。 不过上面这些也只是初步的想法、幻想。我甚至这样做的工作量将是巨大的,没有3、5个月,估计连个影子都看不到。所以还是要坚持每天写上一写,才有可能将这个美好的“幻想”尽可能想着“现实”实现出来。

wxWidgets似乎没有事件广播机制

wxWidgets的任意object产生了事件之后,似乎只能向父层传递,并且“一路向上”的往上传递。既不能向下传递、也不能横向传递。换言之,wxWidgets的事件是不能进行“广播”的。虽然没有通读过所有关于wxWidgets的文档,但是感觉上面的结论应该是准确的。 如果将整个窗口及上面所有的组件看做一颗“树”,任意节点上产生的事件,在“不能广播”的前提下,唯有这个节点的“父亲”是唯一的。 如果这个事件可以“向下传递”,那么任意节点下面的子节点有可能是多个,向下传递意味着就是“广播给所有的子节点”。 同理,如果能够横向传递,必然是先从当前节点向上传递给父节点,再由父节点广播给所有子节点,才能令兄弟节点收到消息。或者换一种想法,任意节点的兄弟节点也可能是多个,所以“横向传递”意味着有多个兄弟节点等待接收事件,由成了广播机制。 所以“没有广播能力”就意味着不能向子节点和兄弟节点发送事件。反之:不能向子节点发送通知就是在说不能进行广播。两种表述是一样的,就是不能只依靠事件对项目进行彻底的解耦开发。 没有广播机制,程序的解耦程度会大打折扣。我现在就遇到了好几处问题。总要迂回着完成事件的传递、或由“中间人”帮忙进行控制的操纵,代码写的凌乱不说、更重要的是这样“千回百转的羊肠小路”做控制的纽带,总担心日后会忘记它们之间的联系。 另一方面,这条“羊肠小径”无论多么的纵曲幽深,都一定不能中断。但是对于复杂的窗口应用程序、尤其是还在不断调整的开发阶段,页面上的控件总会不断地调整、新建、删除,不断的做出重新规划,每一次规划都要想着其中不知有多少“羊肠小径”需要连带着调整。既痛苦、又易出错。 虽然现在我用了一个全局单例来缓解问题,令代码可以尽可能的解耦;也缓解了组件的迁移需要不断调整众多“羊肠小径”的尴尬,但终归觉得还是不完美。此时此刻,这个痛苦的问题,着实令我有些疲倦。

wxWidgets框架下程序的正确终止方式

我正在基于wxWidgets3.2.5写一个小工具,这个小程序并非只有一个MyFrame,而是可能会在不同的场景中有不同的窗口被创建并显示出来、仅仅依靠Frame的Close按钮,是不能将程序真正结束掉的,即使有一些方法可以将程序终止掉,但用得不对则可能导致出现内存泄漏。虽然程序终止的时刻产生内存泄漏并不是什么问题,因为泄露的内存最终由系统回收了,但找到正确的程序终止方式,终归是有益无害的。 待续 update 2024.05.15:因为我最终决定放弃使用wxWidgets框架进行软件的开发,所以这篇文章的“待续”部分也就没有必要写了。

std::vector<T>.size()返回的是size_t类型

一、遇到的问题 今天写程序的时候代码运行崩溃,提示format specifier doesn’t match argument type。因为我刚刚接触C++,所以在这个问题上花了不少的时间。 代码如下: 代码非常的简单,但是一旦编译运行,就会出断言错误提示。最终发现原来vector<T>.size()返回的不是int类型,而是size_t类型,所以在wxLogDebug()中因为存在着断言,发现类型不同就报错了。而后面for()循环里面虽然不报错,那是因为for()循环里面相当于是对int < size_t进行判断,没有断言语句所以不报错,并不是因为真的没有错误。 (int < size_t)这个判断语句虽然在多数情况下都可以正确的执行,但安全起见,还是应该重新定义,改成for (size_t i=0; i<test.size(); i++)这样比较稳妥。 再回过头看wxLogDebug()里面正确的写法,就是使用%zu代替%d,这样格式化字符串的时候接受的参数类型就是size_t了。所以最终代码正确写法如下: 二、size_t和int有什么不同? 上面遇到的问题并不复杂,只是因为以前没有碰到过,所以浪费了一些时间。接下来的问题更令我感兴趣:size_t和int有什么不同吗?既然说size_t是无符号、且与当前系统最大可用内存匹配数,为什么不能直接用unsigned int代替呢? 原因是为了令代码在不同的平台上移植时更加的方便。size_t的确就是unsigned int,但具体是int16、int32还是int64并不一定,在不同的处理器、不同的系统上,size_t表示的范围是不同的。 例如在32位系统上,size_t是unsigned int32,而在64位系统上它的范围则是unsigned int64。…

系统托盘菜单点击之后500毫秒内的残影

想用C++实现一个简单的屏幕截图工具的开发,工具程序是放在系统托盘(IconTray)中的,然后通过鼠标点击托盘图标弹出菜单,再点击菜单中的截屏菜单,完成截屏操作。 软件界面如下: 一旦全屏截屏或框选区域截屏点击之后,程序就开始运行,将当前的屏幕DC保存下来,然后借助memDC将屏幕DC复制到一个bitmap中,再完成写盘操作。看上去是个非常简单的需求。 但是在Windows系统中(Win10),这个icontray的弹出菜单在点击之后并不会立即消失,我这里感觉大约会有500ms的减淡消失效果,最终导致截屏保存下来的图像,总是会带有被点击菜单的残影,如下图: 在程序里面处理其实可以有很简单的方法,就是做一个延迟处理,例如我现在延迟了0.6s、也就是600ms之后在创建screenDC: // 立即对当前屏幕进行全屏截屏操作 void MyApp::CaptureWholeScreen(wxCommandEvent&amp; event) { wxString _imgSavePath = DatetimeUtil::getTodayScreenShotPath(); if (PathUtil::dirExistOrMkdirSuccessful(_imgSavePath) != true) { wxLogDebug("截图存储目录不存在且无法创建,无法完成截图操作"); return; }   // 延迟0.6秒后将整个屏幕DC保存下来,待后面按显示器切割使用 //…