善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。

2026-01-15

逻辑的奴隶?这次我把AI领进了死胡同

最近有同事问我:为什么公司旧版软件在执行某个特定操作时,屏幕上的一部分 UI 就不动了?

说实话,这套旧代码现在成了公司里的「考古遗迹」。唯一深谙此道的同事已经财务自由,移民香港了。我对这个细节也并不非常清楚,只能去看代码。相关代码错综复杂,看得我眼花缭乱却毫无头绪。

想到最近 Google Gemini 3 Pro 的强劲表现,我索性把代码全扔给了它,开启 Pro 模式进行深度分析。

为了「提高效率」,我在 Prompt 中加入了自己的判断:「我认为是相关的 Windows 消息被过滤了。」在我看来,这再自然不过——既然界面停下了,那逻辑上一定是忽略了导致绘图的消息。我只是找不到过滤逻辑的标志位而已,而这事正适合让 AI 靠「蛮力」深挖。

从逻辑上讲,AI 显然非常「同意」我的判断。它一头扎进了我给出的代码里面,然后得出了一些似是而非的结论。我核对后发现,有些结论可以直接排除,有些则根本不符合事实。这意味着,顺着我的思路走下去,连 AI 也找不到真正的原因。

对话进入了死胡同。而 AI 在这种情况下似乎不知道「退出来」,只是机械地一次又一次给出「可能是……原因」的无效推测。最后我不得不靠调试来自行解决问题。

真正的原因,是那位同事在软件界面上遮挡了一个不进行重绘的透明窗口。

图片由 Google Gemini 3 Pro + Nano Banana Pro 生成

事后复盘:我在这里犯了一个典型的错误——我不该在一开始就说出我目前为止的判断。

我反思,如果当初少说一点,让它自行研究,结果会不会更好?我们一贯以来对于 AI 的使用方法,基本上都是「让它知道得越多越好」。我甚至曾这样建议别人:『别隐瞒,把你知道的东西事无巨细都告诉它,哪怕你认为没用的也要告诉它。』

这个原则本身没错,但前提是:你告诉它的是「事实」,而不是「错误的观点」。

很多人类尚且无法区分「事实」与「观点」,AI 可能就更不知道了。在我看来,我的提问还有改进的空间:

  1. 明确标注信息属性: 在 Prompt 中清晰指出哪些是明确的事实,哪些是模糊的现状,哪些仅代表个人的观点和推测。
  2. 允许 AI 「退后一步」: 对于 AI 而言,这只是在做题。Prompt 越具体,求解范围就越窄。我们不能只是把 AI 领到迷宫门口,然后让它进行探索,却不允许它「爬墙头」。

人类会犯错,然后学到东西。AI 也会。如果 AI 能「对抗」人类的意志,我不知道这是好还是坏?

总之,共同进步吧。

2026-01-13

Internet好像也没有那么危险嘛

图片由 Google Gemini 3 Pro + Nano Banana Pro 生成

周日晚上,突然接到公司同事的电话,说我手上有一台服务器「被人攻击」,所以机房要把外网IP给黑洞24小时。

挂了电话之后,想想不对,那台服务器并非生产环境,平时都是内部自己用,要被攻击也轮不到它。

这台服务器,主要都是运维同事在用,我自己了解不多。去年年中,运维同事辞职,我更是早已不负责相关业务。这台服务器有点「三不管」的感觉:登记簿上还挂在我名下,最近没什么空去推进蛮复杂的交接工作,所以还没轮到它;实际管理者没什么能力进行管理,主观意愿上也不想去管;它暂时也没什么多大用处,几乎算是「闲置」状态。

然而,我总觉得自己对它还有「责任」,于是走跳板机从内网连上去又看了看。这一看就让我冒冷汗了。上面居然有个squid,而且貌似是「向全世界开放」的状态。让Google Gemini帮忙统计了一下access.log,发现晚上出事前一分钟就产生了3GB的流量,峰值可能确实把机房设定的阈值给顶破了。所以这应该就是直接原因了。

可是,我不记得自己有在这台机器上搭过squid,也不记得运维同事搭过。他是个谨慎的人,应该不至于干出这种事情。那是谁干的?

再仔细看,简直大汗淋漓了。还有一个未能投入工作的OpenVPN,以及一个名叫proxy的账号,gid是0。完了。什么时候被黑的?!

这个proxy账号看起来是2021年创建的。日志已经灭失了一大半。我只能根据仅有的一点线索,去拼凑还原当时的情况。

「黑客」看来轻车熟路,一上来就登录了root账号,随后创建proxy,设置sudo,然后就去安装squid。

但接下来的情况让我迷惑不解。他光是开防火墙3128端口就折腾了半天,安装squid时也一时输错成了apt-get(这台服务器是CentOS)。装完squid设了allowed_ips,但conf中却大手一挥设成allow all就走了,OpenVPN也是安装了一半就放弃了。前面半截太过顺利,后面半截却画风一转,这不对劲。

我们用的密码都是64位随机字符串,SSH端口也开在非标准端口上。如果这台服务器这么容易被攻破,那么我们剩下的服务器也是凶多吉少。剩下的时间,我都在审查和反省所有可能的入侵路径。还是想不明白,当时到底什么情况?最后凌晨两点钟,实在熬不住,洗澡睡觉去了。


第二天到了公司,我按计划去翻邮件,看看有没有可能出事前是弱密码,后来才改成的强密码。结果这一翻,就发现了「真凶」,也算是一场乌龙。

2021年的时候,当时老板找我要了一台服务器的root权限,说是他儿子要用。
他儿子当时参与了我们的一些项目,也算是「实习」吧,后来去英国读书去了。要这台服务器,是说方便参与维护,当时公司里面还真没人来维护那些东西,所以我也就把这台不太重要的服务器给他了。看起来,当天他儿子拿到root密码,接着就动手了。

大学生嘛,做事情没顾那么多,也是可以理解的。谁还不知道自己大一的时候是个什么样子呢?估计他当时折腾完以后用过一阵子,然后就把这事给忘了。现在他早已毕业,无论是在当地工作或回国发展,总之应该也用不上这个东西了。这代理就这样一直开放在Internet上,不知道有没有人发现,估计没有。虽然日志有限,但看起来2025年一整年都没人用过,直到这个周日。大概总算被什么人给扫描到了吧。

这事说起来就是个内部管理问题。这种事情,就算放在现在,我感觉也没法拒绝。老板自己也大概把这事给忘了。只能是吃一堑长一智吧,以后「出借」的东西要加强审计。以及不要认为别人都会好好善后。哪怕他真的会,但有时候也会偷懒啊。

话说回来,貌似Internet好像也没有那么危险嘛?一个完全开放的HTTP代理,3128端口也是知名端口,居然存在了快五年才被人第一次发现。或许现在ProxyHunter已经没人用了,但是我国政府的扫描器不是一直在巡天么?GFW果真对外不对内?所以啊,世界还真的是一个大草台班子吗?

2026-01-08

关于明尼苏达ICE枪击案的个人评论

看了BBC上关于明尼苏达ICE枪击案的视频。不同的媒体释出的版本,各位也可以都去YouTube上搜一下看看。

https://www.bbc.com/news/videos/cx2ypz4zjvxo

首先说一下个人感觉:纯粹就是谋杀。当然,非要说误杀也行,毕竟动机无法揣测。但肯定是「杀」,不是「自卫」。

被枪击的车辆一开始挡在皮卡前面,然后ICE的人走过去要拉车门,已经拉了一两下了。没拉开,显然司机锁了车门。但驾驶位的窗是开着的,我估计双方也是在对骂。从皮卡司机位下车的那个ICE显然更激动,虽然副驾驶位的同事有做手势让他Calm down,但他把手都伸到车窗里了,看最后那一下的动作,感觉要去从内侧开车门。

没人愿意这样被人拉下车,而且此时也没人拿枪指着司机。司机看起来是想要离开,至少是想要摆脱眼前这种局面。如果换成是我的话,被不怀好意的人包围,并且有人开始拉车门,估计也不会愿意呆在原地。

从轮胎的动作看,是一个类似三角调头的操作,往左后方倒车,然后挂D挡往右前方前进。右边的车的驾驶侧车门是开着的,司机为了避让,不可能一开始就把方向盘往右打死,这也是很正常的事情。并且,换档的时候,打方向盘只有一只手,操作速度没有那么快。因此方向盘一开始看似只是回正了,但随后也就朝右了。而且总之在开枪者决定开枪的那个时刻,车辆向右拐,避开他的趋势,已经很明显了。

开枪者一开始并未持枪,并且明显可以躲开。但他的选择是拔枪,然后跟着车辆的移动方向去前凑瞄准,随后在即将失去射击角度的那一刹那开枪射击驾驶员。正常人要是为了保命的话,应该是连滚带爬地向后退,至少上身会是向后闪避的状态,双手伸出试图阻挡车辆,而不是想着去掏枪。

开枪的一瞬间,可以看到开枪人的两条腿的站姿

掏枪干什么呢?在那个时候,即使他打死了司机,一枪爆头,如果车辆真的是正对着他撞过去,在那个距离上,他也无法避免被撞。但司机只是在向右转弯,并没有去撞他的企图。开枪者此时甚至已经错开一个身位了。事实上,开完枪后,他也很简单地就躲开了失去控制的车辆。故此,很难让人相信他开枪的目的是为了「自卫」,特别是作为一个「有经验」的Agent。

我个人感觉他就是想阻止司机离开,本意可能是想击伤司机,没想到一枪毙命。当然也可能是PTSD之下发P疯了。这些都是可以「辩解」的点。但这改变不了事实:他就是想通过开枪达到自己的某个目的,而那个目的绝对不是「保护自己的生命安全」。

中国大陆在2007年也发生过「储户被押钞员一枪爆头」的事件,当时我还写过一篇Blog。过了这么多年,事情是在越来越好,还是越变越糟,各位心中自有评断。无论如何,这些都是不应该发生的悲剧。牺牲究竟能不能换来一点什么,从前的我,希望有,现在的我,但愿有吧。

2025-12-24

订阅了Google AI Pro

看过一些朋友的推荐,也跟Google Gemini一番探讨之后,今天终于下决心订阅了Google AI Pro。

可以有更多的Google Gemini配额了,包括「思考」和「Pro」,后者貌似就最近两天才加上的。以及,更多的Nano Banana / Nano Banana Pro图片配额。还可以用Veo3.1做视频。免费版的Deep Research配额我曾经用满过,所以还真是有必要成为付费用户。

Google全家桶里面也可以用Gemini了。我记得有印象在Gmail里面看到过Gemini的图标,但今天去找的确找不到了。莫非是当时Google放的鱼饵?

另外,对我而言还有一个挺有吸引力的福利:2TB存储空间。太太的Google Photos空间早就满过了,删了一些视频才降下来。我自己也不敢拍太多照片和视频,就怕哪天空间满了出事情。这问题甚至影响到了我的某些旅游的体验,现在不成问题了。

详细介绍见这里,不过我不知道算不算是最新的:https://support.google.com/gemini/answer/16275805

我是选的「包年」方式,因为感觉一旦用上了应该就「回不去了」。
首年$100,之后每年$200。包月正常价是$20,现在也有试用优惠。不知道是不是年底或圣诞节特别搞的活动。反正都要买,不想等几天错过了,就下单了。
我个人是不太在意这些小恩小惠的,何况已经很划算了。

说起来,Google AI Pro应该是普通中国人最容易「够到」的顶级AI品牌的付费服务了。ChatGPT Plus之前对中国用户想尽办法封锁,还砍单封号来着。直到现在也没见得有多方便,甚至连「安全」都不一定算得上。我曾经形容为「被中美混合双打」,为了用个靠谱的AI也真是难为了。

图片由Google Gemini 3 Pro + Nano Banana Pro生成

当然,对于普通人,我一律是劝他们尽量多用AI,哪怕是国产AI。
有在用总比没有在用要强。面对熊,你只需要比同伴跑得快就行。

AI问:那如果是面对狼群呢?

我说:你差不多得了。 

2025-12-22

丢失的Blogger铅笔小图标

图片由Google Gemini阅读本文后生成,真是厉害

之前在自己折腾Blogger,大概就是2023年或2024年把Blogger重新翻出来写的时候,一不小心,把每篇Blog下面的「铅笔」小图标给整没了。

这个小图标是管理员才能看到的。直接一点就可以进到编辑界面。对于我这种时常在回顾自己文章时发现错别字,需要马上去修改的人而言,真的是很方便。把它弄丢了以后,我每次就得根据文章的标题或标签再去Blogger后台找到那篇文章。有时候某些想法只是一瞬间的事情,过了就没了,对于我而言,更可怕的是「过了就忘了」,这也太可怕了!

我一开始还以为是自己之前在编辑主题的时候不小心把它给误删了。但今天想起这事,下决心解决,去看代码的时候,才发现好像不是这么一回事。代码都在里面,但就是不起作用了。

这是原来的代码:

<b:includable id="postQuickEdit" var="post">
  <b:if cond="data:post.editUrl">
    <span expr:class=""item-control " + data:post.adminClass">
      <a expr:href="data:post.editUrl" expr:title="data:top.editPostMsg">
        <img alt="" class="icon-action" height="18" src="https://resources.blogblog.com/img/icon18_edit_allbkg.gif" width="18" />
      </a>
    </span>
  </b:if>
</b:includable>

看到这里我就有点犯嘀咕。cond明显是条件,该不会是post.editUrl没东西吧?
我把cond改成一个必定成立的条件,放到页面上一试,还真是。图标出来了,点击没用处,没能跳到编辑页面。问Google Gemini,它说新一些的主题里面这个图标的确已经被隐藏了。看来Google是铁了心要把这个功能干掉了。

我想自己整,但发现真正的编辑页面的URL好像有两个ID我不知道哪里能拿到。Google Gemini给出的修改方法语焉不详,而且看起来也没有说到点子上。算了还是求助于传统的Google吧。

搜到的第一个贴子是Google自己的页面,Blogger的Support论坛:
https://support.google.com/blogger/thread/242102130/editurl-the-url-of-the-edit-form-for-this-article-deleted

里面有提到这位大神,是一个法语的Blog:
https://bloggercode.orbiona.com/2021/10/faq.html

后面有个英语Blog也提到上面那篇:
https://too-clever-by-half.blogspot.com/2022/08/the-case-of-missing-pencil.html

法语对我而言有不少困难,还好有Google Translate。
我翻到解决方案一节就知道了:看起来那个法语大神给出的就是我想要的东西。
简单地说,把原来的那段代码替换为:

<b:includable id='postQuickEdit' var='post'>
  <!-- /2021/10/faq.html -->
  <b:with value='data:view.isPage ? "blog/page/edit/" : "blog/post/edit/"' var='path'>
    <span expr:class='"item-control " + data:post.adminClass'>
      <a expr:href='data:blog.bloggerUrl path (data:path + data:blog.blogId + "/" + data:post.id)' expr:title='data:top.editPostMsg'>
        <img alt='' class='icon-action' height='18' src='https://resources.blogblog.com/img/icon18_edit_allbkg.gif' width='18'/>
      </a>
    </span>
  </b:with>
</b:includable>

就可以搞定了。

原因据说是Google慑于第三方Cookies的限制,没法让前台(blogspot.com)和后台(blogger.com)在域名不相同的情况下还能把管理员的登录会话串起来,索性就把整个功能废掉了。

解决方案的原理也很简单:你不让我跳编辑页面,我就自己组URL。知道BlogID,也知道PostID,还有什么做不到的呢?

当然,这里头还有一些诸如「管理员权限的判断」等小东西。按照「八二原则」,真正花时间的往往是在这些事情上。掌握了核心技术,路只走了20%而已。这位法语大神甚至已经把「页面」和「博文」的差别也搞定了。我就不折腾了,直接用她的吧。

照例吐槽一下:搜索结果中,简体中文的页面依然是一无所获,繁体中文有两篇讲到了这个事情,其中一篇涉及到了核心问题,不过最完整的解决方案还是法文以及英文的页面。简体中文在世界文化交流领域里面就是一个小角落,不管卖出了多少玩具电动车,都改变不了这个事实。不要再夜郎自大了。

2025-12-16

如果你还认为自己是普通人

我不认为自己是什么「普通人」。就算我哪怕曾经有过这样的想法,但不久前以及现在也不再这样认为了。

如果你还自认为是个「普通人」,那么,最好赶紧跟下面这种人区分清楚。

你可以说「这个人根本不是什么普通人」,也可以声明自己「并不是普通人」。如果你已经了解此事,但仍然还是默不作声,那么这份恶,你就也有一份。

祝你去世

没有人是「普通人」。你是独一无二的,你就是你。

2025-12-04

这是一个所有人都在推卸责任的时代

某家银行下辖的期货公司发来公文,说我们的软件存在着「DLL劫持」漏洞,限期修正,催得很急。

说起来跟《鬼子来了》一样,翻译官最该枪毙。DLL是什么东西,老爷可能不懂,但是一听到「劫持」二字就坐不住了。劫持?打劫?谁打劫?匪徒?要劫谁?黄四郎的觉都要睡不着了。

是不是漏洞,技术上说,当然可以算是。该不该改?可以改,可以不改。对方拿了个自己生成的TextShaping.dll往我们软件的安装目录下一放,Calc.exe被调起来了,就说有问题。然后我拿去往Steam的安装目录下一放,也有问题。人家9700万月活,四千多万的并发在线人数,似乎并不担心。

Foxmail也一样有问题。而且Foxmail比Steam还大条,它默认推荐的安装目录可不是什么%ProgramFiles%之类,说起来问题更严重。但是它们都没改,或者说,没被要求改。为什么?因为没有「主」。都是用户自己下载,自己安装,自我的决定。出了问题,没有人寻死觅活找别人担责。

其实通达信也一样,别看它貌似防了一手,但它仍然可以被DLL劫持。关系到同行,我就不展开细讲了。Resource DLL各位自行了解一下。

最鸡贼的是招行的网银。以前我就吐槽过它,貌似为了「安全」而无所不用其极。这次我又发现它的一个做法:为了防止DLL劫持,它索性把自己放到Windows的系统目录里面去了。

可以可以,相当可以

这样当然可以防止DLL「劫持」,因为它直接把自己和操作系统的安全性绑一块儿了。相当于搬到元首家里去住下了,要死一起死。

解决方案不是完全没有,但本文本质上是一篇吐槽文,不是技术类文章,因此也不展开细讲。

说到底,这些所有的事情,都是为了推卸责任。期货公司真的是为了客户的「安全」着想吗?让这种软件能在客户那已经千疮百孔的机器上「带毒运行」,真的好吗?还真以为自己是常山赵子龙啊?能在恶意环境中杀个七进七出,片叶不沾身?

它们其实只是为了在客户真出事找它们寻死觅活威胁要跳楼的时候,可以拿出一些文件来,对着来「监察」的「钦差」们说上一句「你看我该想的办法都想了,剩下的就不是我的问题了。」

然而问题解决了吗?解决个锤子!


今天又收到一篇公文,是要向我们「确认」软件是否有采集客户的MAC地址并上报。

听上去是个人信息收集的问题,不过在这个行业中,有强制要求必须要这样做的,所以这个行为是合法合规的。要确认的,是我们不能「漏报」。

然而,这个「确认」其实是在耍滑头。自己不想去通过测试来确认,恐怕也无力去确认,于是发文件让供应商签字画押。本质仍然是找人担责。

我本来写了一大段,后来又默默地删掉了。对老爷能说些什么呢?

我觉得「MAC地址能自己动手改」这应该是一个老爷可能不知道的常识。老爷本就应该是不具备什么常识的,所以我就不去败兴了。我能告诉他什么是拨号上网吗?他去一搜,到处都是PPPoE,回过头来跟你说Modem也有连接的网卡。他晓得个锤子,他连Modem为什么叫「猫」都不晓得。

他要押,你就画给他,画个小乌龟,带尾巴。


太太这些天,在办交强险退保的事情,办得劳神劳力,最后我让她算了,就当给保险公司的老爷老娘们拿去买伟哥了。

说起来是很有理有据的事情。没起保的交强险可以退,相关的说法网上一搜一大把,都是支持的。AI分析也没有任何问题,我甚至让Google Gemini和ChatGPT都分别做了Deep Research,法律证据也都很充分。甚至太平洋保险自己都有案例解读,监管层2023年也有相关的Q&A。然而就是办不下来,四处碰壁。

投诉到太平洋客服没什么用,开口就是公司规定。去银监会投诉,只是把事情给你转回去。其实又是踢皮球到太平洋保险,说法还是同一套,总之就是不给退。

在网上刷到一位有相同经历的程序员。看来保险行业内部,特别是客服岗位,恐怕应该是学习过这种案例了。如果松了口承认了相关法规,就得受罚,而且公司层面说是「补偿」而不是正常的办理流程,也就是「口子不能开」,开了就是客服个人的责任。

监管层说是可以退,然后又让个人去跟保险公司「协商」。这情景活脱脱就像我当年去电脑城找12315投诉商家欺骗消费者,然后老爷过来指导销售「你快点跟消费者解释你们都有些什么额外的成本」。我在猜,大概体系内部其实是不允许退的,对外说可以退,对内严惩相关人员。医保不也一样?


其实吧,我就把话说重一点,也说得大一点。从三年疫情就可以看出来,共产党和中国政府其实根本不是在管理,他们也没那个能力。他们就是「责任驱动」,你也可以说他们是被「压力驱动」的。他们所做的一切,都是为了「卸责」,逃避危险。所有事情的动机,都可以用「给自己少些麻烦」来理解。

尽管有的时候,他们貌似反而制造了更多的麻烦,但你如果试图站在他们的立场去想想,就会发现,那是因为他们觉得如果不这样做,就会有「存亡危机」。「人无远虑,必有近忧」,话是没错,然而鼠目毕竟只有寸光,一切终归是偷来的。

看穿了这一层,很多事情,你就可以理解了。虽然逃避责任最好的办法是不担责,不在其位,自然就没责任了。但是有一个词叫做「秋后算账」,他们是最怕的。当年他们这样干过,所以自己最清楚。后面上台的人也不想为现在的烂摊子买单,那么必然有人要担责。猜猜会是谁呢?因此,为了不被秋后算账,现在必须抓牢权力,而且永远也不能松手。非不愿也,实不能也。

上行,所以下效。何况人性本来就是趋利避害的。现在没有利可以趋了,那害总是要避一避的。故而,造就了这么一个所有人都在推卸责任的时代。

即使在公司里,也能嗅到这种氛围。上了点年纪的同事,「混」得最好的,就是那种浑身是刺,搞得别人不想理他的人。其他人也在眼馋他的「清闲无事」,纷纷向他学习。做事情都是趋于保守,少做少错,不做不错。长此以往,一个民营企业,会有什么下场?我不知道,但连老板想的都是「先把这几年熬过去」,以及「找死不如等死」。

如果参透了这些,或许能在眼下处世更加自如一些。我虽然已经看破,但无意为伍。且也先「保守」一下,把自己的心田防守扎实一些吧。


洋洋洒洒写了这么多,其实一句话「社会逐渐趋于保守」就可以概括。这应该也是大家都基本能认同的事情。只不过,最近遇到的想吐槽的事情,一下来了好几件,还是想拿出来说一说,所以絮絮叨叨写了这么多。

以及,有些话,如鲠在喉,不吐不快。真的说出来了,心里竟然舒服了一些,可见还是有毒。

2025-11-13

读得太少,写得太多

图片由Google Gemini生成

这两天在反思,觉得自己最近光是在写,阅读不多,进货少出货多,因此写得也有点没感觉。

自己的写作水平,大概也就这样,应该提升不了多少啦。不过或许多读点东西还是会有一些用。挺怀念年轻的时候,读过的东西,马上就会反映在接下来的写作风格的变化里面。虽说是「没有形成自己的风格」的表现,但也说明「可塑性强」,一切都还没有定型。现在到了人生下半场,没什么指望了。

而且,我现在只是看「故事」居多。只挑自己爱看的,看得舒服的。例如推理小说、科幻小说,或者别的什么小说。正经的学术性书籍、严肃文学,下场可以参考那本在我书架上放了十多年还没翻过的《国富论》。

何况,即使是山冈庄八的《织田信长》,我也前不久才开始看。买的时候应该跟《国富论》是差不多的年代。摸着已经发霉的书脊,复杂的感情油然而生。也不是懊悔,毕竟也还来得及看完。更多的可能是恐惧吧,如果已经开始插管,不知道还能有多少时间留给我。

时间太少,要做的事情太多。或许这才是我昨晚那回想不起来的噩梦的真实内容?

那就把时间拿去做点真正重要的事情吧。因此,接下来我的输出频率可能会下降一些。反正我也不是一个当作家的料。有句鸡汤文,大意是「造物主给了人两个耳朵一个嘴巴,就是叫人多听少说」。细想有点扯,但是多沉淀一下也不错。

2025-11-11

你可能从未注意过的MFC陷阱:模态对话框禁用机制的局限性

在最近的一个软件开发案例中,遇到了不容易处理的情况。

在我们的软件中,有那种一直浮在界面上的非模态对话框,例如那种浮动的工具面板。但是,这种窗口,貌似不会在主窗口弹出模态对话框的时候被Disable。如果模态对话框是在这个非模态对话框中弹出的,那没有问题。

用VS2013和升级到最新的VS2022各写了一个测试程序,发现这就是MFC的默认逻辑。
在主窗口上放了两个点击后各自会弹出非模态对话框和模态对话框的按钮。先弹出非模态对话框,然后再去弹出模态对话框。此时主窗口被Disable,无法响应鼠标、键盘消息。但非模态对话框不受影响。

本想靠调整产品设计「容忍」过去。但问题在于,如果在保持模态对话框弹出的情况下,去把非模态对话框先关闭了,那主窗口会被Enable。此时它跟模态对话框之间都能响应鼠标、键盘消息,效果就好像之前弹出的模态对话框变成了非模态对话框一样。这个时候就会有「后果」了,产品设计再怎么调整,也没法让软件在这种乱了套的情况下还能工作正常。


在网上Google了半天,不要说解决方案,连问题都没人提到。讲解模态/非模态对话框的文章有一些,但都是很入门的介绍。只是从「使用者」的角度去讲用法谈区别,并没有涉及我遇到的问题。很少从原理角度进行说明,更没有去分析源代码。大概MFC现在用的人真的不多了吧?

图片由Google Gemini生成

还是得自己动手,在DoModal()处下了一个断点,单步跟踪进到MFC的代码里面看了一下,就明白了。以下代码来自VS2013,VS2022我貌似没有安装MFC源代码,但从表现上看二者在这个逻辑上应该相差无几。我们一起来看看CDialog::DoModal()到底干了些什么事情吧:

INT_PTR CDialog::DoModal()
{
	// can be constructed with a resource template or InitModalIndirect
	ASSERT(m_lpszTemplateName != NULL || m_hDialogTemplate != NULL ||
		m_lpDialogTemplate != NULL);

	// load resource as necessary
	LPCDLGTEMPLATE lpDialogTemplate = m_lpDialogTemplate;
	HGLOBAL hDialogTemplate = m_hDialogTemplate;
	HINSTANCE hInst = AfxGetResourceHandle();
	if (m_lpszTemplateName != NULL)
	{
		hInst = AfxFindResourceHandle(m_lpszTemplateName, RT_DIALOG);
		HRSRC hResource = ::FindResource(hInst, m_lpszTemplateName, RT_DIALOG);
		hDialogTemplate = LoadResource(hInst, hResource);
	}
	if (hDialogTemplate != NULL)
		lpDialogTemplate = (LPCDLGTEMPLATE)LockResource(hDialogTemplate);

	// return -1 in case of failure to load the dialog template resource
	if (lpDialogTemplate == NULL)
		return -1;

	// disable parent (before creating dialog)
	HWND hWndParent = PreModal();
	AfxUnhookWindowCreate();
	BOOL bEnableParent = FALSE;
	CWnd* pMainWnd = NULL;
	BOOL bEnableMainWnd = FALSE;
	if (hWndParent && hWndParent != ::GetDesktopWindow() && ::IsWindowEnabled(hWndParent))
	{
		::EnableWindow(hWndParent, FALSE);
		bEnableParent = TRUE;
		pMainWnd = AfxGetMainWnd();
		if (pMainWnd && pMainWnd->IsFrameWnd() && pMainWnd->IsWindowEnabled())
		{
			//
			// We are hosted by non-MFC container
			// 
			pMainWnd->EnableWindow(FALSE);
			bEnableMainWnd = TRUE;
		}
	}

	TRY
	{
		// create modeless dialog
		AfxHookWindowCreate(this);
		if (!CreateRunDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), hInst) && !m_bClosedByEndDialog)
		{
			// If the resource handle is a resource-only DLL, the dialog may fail to launch. Use the
			// module instance handle as the fallback dialog creator instance handle if necessary.
			CreateRunDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), AfxGetInstanceHandle());
		}

		m_bClosedByEndDialog = FALSE;
	}
	CATCH_ALL(e)
	{
		TRACE(traceAppMsg, 0, "Warning: dialog creation failed.\n");
		DELETE_EXCEPTION(e);
		m_nModalResult = -1;
	}
	END_CATCH_ALL

	if (bEnableMainWnd)
		pMainWnd->EnableWindow(TRUE);
	if (bEnableParent)
		::EnableWindow(hWndParent, TRUE);
	if (hWndParent != NULL && ::GetActiveWindow() == m_hWnd)
		::SetActiveWindow(hWndParent);

	// destroy modal window
	DestroyWindow();
	PostModal();

	// unlock/free resources as necessary
	if (m_lpszTemplateName != NULL || m_hDialogTemplate != NULL)
		UnlockResource(hDialogTemplate);
	if (m_lpszTemplateName != NULL)
		FreeResource(hDialogTemplate);

	return m_nModalResult;
}

看到高亮的代码应该就能明白了。MFC在弹出模态对话框的时候,只是把hWndParent给Disable了。然后如果发现主窗口还没被Disable,再补上一刀。后面这个逻辑应该就是为了应对在非模态对话框中弹出模态对话框的情况。

看到了这些逻辑,就可以明白本文最开始讲到的情况是情理之中的。模态对话框只管住了大BOSS和父亲,对于兄弟和叔伯之类,都没有去管。如果情况复杂一点,例如非模态对话框中又弹了第二层的对话框,那么不管这个对话框是模态还是非模态,接下来都有机会整出点问题。


知道了问题的原因,接下来就要寻找靠谱的解决方案了。肯定有人解决过这类问题,因为有些软件没有这种问题。但不知道是因为觉得问题太简单了不值得说,还是想藏着掖着?

希望是前者。因为真正的解决方案确实也很简单。

之前有同事尝试解决这个问题,办法是让模态对话框去通知所有的非模态对话框(或者说需要被Disable的窗口)自行Disable,用了我们软件内部与Windows消息相互独立的另外一套通讯机制。

但这个解决方案有一些问题:模态对话框除了自己开发的那些,还有系统提供的MessageBox、文件打开对话框等,另外还包括第三方库中的那些,都是我们无力触达的。所以虽然它花了很多力气让对话框们去多重继承一个基类,但还是没法彻底解决这类问题。

回头想想,主窗口不是总是会被Disable的么?那么监听主窗口的WM_ENABLE消息,在事件处理函数中把那些需要额外Disable掉的窗口集中处理掉,不是就可以了?

实际做起来也很简单,基本上就是在主窗口的OnEnable()中用EnableWindow(bEnable)把状态传递过去。唯一需要动点脑筋的,就是让那些需要额外Disable掉的窗口把自己的HWND注册到一个主窗口拿得到的地方,也就是说待处理的窗口列表需要管理起来。

具体的实现代码我就不贴了。重要的是解决问题的思路,具体如何实现,可以有很多种办法,选择适合自己的哪一种就好。相信读到这里的,都是合格的Win32/MFC程序员。

2025-11-07

小目标

去年,儿子上了初中,虽然是「预备班」,即传统说法中的「六年级」,但还是换了个学校,换了同学,也换了班主任老师。总之,一切都是全新的了。

跟小学那个有点坏的数学班主任不同,初中的班主任是教体育的。女老师,精瘦,据说以前是踢足球的,不知道是不是哪个体校退下来的。倒是应该比较受学生的欢迎,特别是男生。

班主任的有些做派,我并不认同。当然我也知道现在基层教育的种种难处,巨大的机构裹挟之下难有个人发挥的空间,所以也不想去苛责这些年轻人。不过她有个事情我倒是蛮想点赞的:她让这些学生每天订一个「小目标」,去完成它,并且在家校本上记录下来。

儿子同学的小目标,来自「钉钉」家校群

预备班很勉强地坚持了一个半学期,这事在儿子身上目前大致算是撂下了。如果我不坚持催促,他是不会去订这个「小目标」的。即使在我的要求之下勉强去做了,也只是把自己偶然做过的一些事情给揉碎了写上一些。这基本上就是在「应付」了,肯定不是老师的本意,也不是我的。要我老实说的话,这种事情在这个年龄段的小男孩身上,的确还是有些勉强。

老师其实还订了一些别的「规矩」,比如按学号轮流在「钉钉」群里发家校本的内容。本意是借机督促每个学生都认真抄写每天的作业要求,然后也给那些偶尔忘抄或忘带的小可怜一个「补救」的机会。然而群里最后一次的发送记录是10月15日,接下来不知道是谁没发,再后面「链条」就断了。

出现这种情况,我也完全能理解。去问儿子什么时候该他发,他也不知道,只是记得他前一个同学是谁。他就只盯着那个同学,人家不发,他也就不把这件事放在心上。玩STL的都知道forward_list断了是啥下场。这规矩订得还是有一些脆弱,应该改成vector或map。


我读初三的时候,语文老师也订了一个规矩,让我们每天回去记录一条新闻。规定了字数,大约两百字左右,不算多,但也显然不是你去把标题一抄就能合规的程度。规矩一出,班上同学一片哀嚎。作文也就六七百字,这相当于一周固定增加了两篇作文的作业量了。

我其实也很头疼。在那之前,我写个作文也是写几句话就开始数字数,尽量加写长点的定语来凑篇幅。如果去记录新闻的每一句话,却又来不及。我看过一点《大卫·科波菲尔》,知道速记学起来没那么容易。当时又没有手机可以摄下来录下来(话说我还真动过用录音机的心思),因此刚开始的一段时间,我也是跟其它同学一样,到了晚上就开始发愁。直到有一天……

那天我记得很清楚。我边吃晚饭边在看新闻联播,进入到最后十分钟的「他国都很乱」环节,播报了一条新闻,是讲联合国核查小组在伊拉克检查大规模杀伤性武器的事情。因为是我相对感兴趣的话题,所以虽然正在吃饭没去记录,但整件事情至少听完以后还能向家人完整复述出来。等我吃完饭准备写作业的时候,才发现只要用自己的话把听到的内容再描述一遍,这个作业是如此的简单。

不需要速记,不需要录音机,不需要每句话每个字都一样,只需要一个大概。我很轻易地就写了六七排,肯定超过两百字了。这个时候我才发现,原来两百字不是太多,而是不够。我很讶异:我都初三了,为什么以前都没学会这么简单的事情?以及,为什么班上其他人也做不到?

后来我在想,或许这就是所谓的「开窍」。事情发展到了一定的时候,窗户纸捅破了,突然一下就明白了。

老师第二天就把我写的东西当着全班的面念了一遍,然后传给班里每个人看。现在想来,当时她心里肯定有一种「我终于等到了」的感觉。那一刻,肯定很有成就感,多巴胺超级加倍。

班上同学们也很快就明白了,毕竟是重点中学,大家也都不是傻子,只是差那一下的棒喝。当天就有明白过来了的人开始模仿,接下来甚至有的家伙开始编「新闻」。当年那种奇奇怪怪的「社会新闻」有很多,不过我们听完之后还是纷纷议论「太离谱了」「肯定是编的」。但是老师也未置可否。回头看看,编的故事显然是更不错的果实,只要编得足够好。

151岁,谁说是高科技来着?

当年我的老师是不是算是给我们设立了一个小目标,我不是很清楚。但是显然,目标不可能一天达到,但是也不能放弃,继续做下去,可能就有收获。指不定是在哪一天,但是放弃了,这事就真的没戏了。

就像丽春院的小姐姐说的:「叫,不一定有客。不叫,就一定没有客。」


儿子经常陷入「小目标写个啥」的忧思。我看到真是又好气来又好笑。要是让我每天只立一个小目标,我自然会嫌少:探索并关注1-2个有意思的Substack频道;写一篇Blog;整理相册;去Steam上淘一个好游戏……简直不胜枚举,数不胜数。不过,儿子看来还缺些时候。我到初三才稍微开了一下窍,不能拔儿子的苗。

何况,儿子也有儿子的烦恼。我看了他最近的英语题目,包括数学试卷。有不少题都是那种「你猜猜我要考你什么」的题目。比如一句英语空两个位置让你填,目的其实是想让你填这个学期刚学过的某个词组。中国考生说不定真能答对,换个英语母语者来答,肯定不知道出题人脑子进过什么shit。

百发百中

这种「先射一箭再画靶子」的事情,从我还是学生起,直到现在,一直就没变过。所以能教出什么来,考试分数高的都是些什么人,可想而知。当公务员肯定是好材料,可惜我个性不适合。

前天儿子一张数学试卷58分,我跟他一道题一道题地看错在哪里。昨天他76分,我跟他说你如果认真答题不粗心,大概也就是这个分数,七十多,运气好能上八十。如果哪天你开始得100了,那我就要担心你的脑壳是不是开始方了。

不多不多,买不了熊猫

算了,咱家都是普通人,还是先从定一个能达到的小目标开始吧,比方说先挣它一个亿……