news 2026/10/6 17:32:27

NX二次开发回调函数uc1616实战:从DLL配置到菜单加载全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NX二次开发回调函数uc1616实战:从DLL配置到菜单加载全解析

搞UG二次开发的人,翻老项目的时候大概率见过这个函数:uc1616。它在NX Open C API里属于老资格功能,但一直到NX10.0都没退役,而且很多公司内部的批量处理、参数修改、报表导出小工具,就是靠“一个DLL + 一个uc1616回调 + 几个菜单项”撑起来的。我下面就从一个最小可跑的DLL工程说起,把uc1616的前因后果、函数参数、VS工程配置、NX加载流程和常见坑一次性讲清楚,适合刚入门NX二次开发、准备接手老代码的人,也适合想快速搭一个“菜单+回调”模板的工程师。

1. 项目整体设计与思路拆解

1.1 uc1616到底是个什么角色

uc1616在NX Open C API体系里属于User Function(用户函数)这一派。User Function是早期UG提供给二次开发者的C接口,函数名基本都是uc开头加四位数字,比如uc1601创建菜单、uc1602添加菜单项、uc1603加分隔条。uc1616是这一族里的“回调函数”,它不负责创建任何东西,而是负责接收通知:当NX菜单栏上某个由我们创建的菜单项被点击时,NX会把控制权交到uc1616手里。

用生活里的例子来理解回调:你去餐厅点菜,菜单挂在墙上(uc1601创建的菜单),你点了某个菜(用menu_id和user_id定位到某个菜单项),服务员不会立刻把菜送来,而是把订单传给后厨,后厨做完了再端出来。后厨就是uc1616,NX就是服务员,点击菜单这个动作就是“订单”。uc1616不需要主动去轮询菜单状态,NX会在合适的时机主动找上门。

所以uc1616的定位是“被NX调用”,而不是我们自己的代码去调用它。这是理解这个函数的核心。很多初学者一上来就想着“在哪调用uc1616”,方向就反了。正确的做法是把uc1616的实现写好,然后通过uc1601注册菜单时把它的地址告诉NX,剩下的事全交给NX调度。

1.2 回调机制里的menu_id和user_id

既然NX会在菜单被点击时调用uc1616,那被点击的到底是哪个菜单、哪个菜单项?答案就是函数签名里的两个ID:menu_id标识是哪一条菜单,user_id标识这条菜单里的哪个菜单项。注册菜单和添加菜单项时,我们手工给每个菜单项分配一个user_id,比如1号菜单项、2号菜单项,后续回调里就能用switch去判断该执行哪段功能。

这种设计在老式菜单API里非常通用,本质上就是“事件源 + 事件参数”的思路。menu_id和user_id就是事件参数。NX不需要知道回调函数内部怎么处理,它只管把这两个ID原样递过来。我们在回调里根据user_id分发到对应逻辑就行。注意menu_id不是我们自己随意指定的,而是uc1601返回的,这是因为NX内部要给菜单分配唯一句柄,我们自己编一个数字进去很可能和别的菜单冲突。

实际写代码时,建议用宏或枚举来定义菜单项ID,比如:

#define MY_MENU_ITEM_ONE 1 #define MY_MENU_ITEM_TWO 2

这样回调里的switch可读性好很多,改菜单项时也不容易把数字写错。等菜单多了、项目大了,这套“ID映射”会越来越重要,否则回调里一堆魔法数字,过一个月自己都不认识。

1.3 为什么现在还在用这套老方案

看到这里可能有疑问:NX10.0都2014年之后的版本了,为什么还要用uc1616这种老式菜单?直接上NXOpen C++ + Ribbon菜单不是更现代吗?这话没错,但实际情况里至少有三类场景绕不开uc1616。

第一类是存量代码维护。很多企业十年前的自动化DLL就是用uc1601加uc1616这种模式写的,功能稳定、集成到了生产流程里,做维护的人往往只需要改一小块逻辑,没必要把整个DLL推倒重来。第二类是快速原型和内部小工具。临时要一个“导出当前部件BOM”或“批量改属性”的功能,用uc1616加几十行代码就能在一个DLL里实现,无需编辑器、无需Ribbon工程、无需UI Styler。第三类是跨版本兼容需求。uc1616这种C接口在很长的NX版本周期里变化很小,一个DLL经常能在NX7到NX12之间来回用(前提是编译器和运行库匹配),这对需要同时支持多版本的团队很有价值。

当然,它也有明显短板:界面是老式下拉菜单,没有图标和分组;如果要做复杂对话框,还得另起UF_UI或UI Styler工程。所以方案选型时要说清楚:uc1616适合做“菜单入口+轻量功能”,不适合做“复杂交互工具”。理解了这一点,用起来就顺手了。两种方案的差异可以看这张表:

对比维度uc1616老式菜单方案NXOpen C++ Ribbon方案
代码量一个DLL几十行即可需要工程模板和Ribbon布局
界面表现传统下拉菜单,无图标分组现代Ribbon,支持图标、分组
依赖复杂度只依赖UGOPEN头文件和两个库依赖NXOpen C++库和UI Styler
高版本兼容性老接口稳定但不再演进官方主推、持续更新
适用场景内部小工具、老项目维护全新项目、复杂交互

2. 核心细节解析与实操要点

2.1 uc1616的完整签名与参数语义

uc1616在头文件里的声明一般是这样的:

int uc1616(int *response, int menu_id, int user_id);

返回值为0表示正常处理完毕,NX可以继续执行后续动作。第一个参数response是输出参数,用来向NX反馈本次回调的处理状态,通常我们在函数开头就写*response = 0。如果确实遇到异常,可以把response置为非零值,告诉NX“这个回调没有正常完成”,NX在日志里会体现出来,但不同版本对非零response的处理力度不太一样,所以不要过度依赖这个状态,该弹错还是自己弹错。

menu_id是触发本次回调的菜单ID,它在调用uc1601注册菜单时得到。一个DLL里可以注册多条菜单,每条菜单都有自己的menu_id,回调里通过menu_id区分是哪条菜单被点击。user_id是菜单项的ID,它和uc1602添加菜单项时传入的user_id一一对应。这里的ID是“回调标识”而不是“位置编号”,即使菜单项在菜单里的位置变动了,只要user_id不变,回调逻辑就不用改。这个特性在维护动态调整菜单顺序时非常有用。

细心的读者会发现,uc1616这个函数名本身也只是一个“约定俗成的回调名”。关键不在于函数叫什么,而在于uc1601注册时传入的函数地址。你可以把函数命名为任意名称,例如myCallback,只要函数签名一致就没问题。旧代码里普遍叫uc1616,是因为官方文档习惯用它做示例,没有规定必须叫这个名字。

2.2 配套的uc1601和uc1602到底怎么用

uc1616不是光杆司令,它必须和菜单创建函数配合使用。最基本的两个是uc1601和uc1602。uc1601用于创建一条带回调能力的菜单,典型用法:

int menu_id = 0; int rc = uc1601("MyMenu", &menu_id, uc1616, &menu_id);

第一个参数是菜单标题,可以带&字符指定快捷键字母;第二个参数传入菜单ID变量,函数返回时会得到分配好的真实ID;第三个参数就是我们写好并要注册的回调函数指针;第四个参数一般也传同一个menu_id变量的地址,用来接收实际生成的菜单ID。如果返回值rc不是0,说明注册失败,常见的失败原因包括菜单标题冲突、NX菜单系统异常等。如果编译时提示uc1601参数个数不对,说明当前NX版本的头文件可能是三参数版本,把最后一个参数去掉即可。

菜单建好之后,用uc1602添加菜单项:

uc1602("Item One", menu_id, 1, 0); uc1602("Item Two", menu_id, 2, 0);

第一个参数是菜单项标题,第二个参数是要添加到哪条菜单(用uc1601返回的menu_id),第三个参数是前面说的user_id,第四个参数是快捷键,传0表示不设置快捷键。如果想给菜单项加分隔效果,在需要分隔的位置调用uc1603,它会把后续菜单项和之前的内容分隔开。这些函数都属于User Function菜单族,头文件都集中在UGOPEN目录下的相关文件里,编译不过时优先检查头文件是否齐全。

2.3 回调函数里的API使用纪律

uc1616虽然是被NX调用的,但它在DLL里做实质性工作时,绕不开NX Open的其他API。这里有一条非常重要的纪律:任何UF_* API调用之前,都要先调用UF_initialize();所有调用结束之后,再调用UF_terminate()。UF_initialize的作用是让当前线程进入NX Open环境,相当于告诉NX“我要开始用你的API了,请把环境准备好”。

省掉UF_initialize的典型后果是莫名其妙的崩溃或返回错误码。如果你在回调里写完一段代码,运行时处理具体功能时也没报错,但不定期崩溃,先检查是不是初始化漏了。反过来,调用了UF_initialize却不调UF_terminate,会让NX觉得这个线程一直占用环境,长时间运行会积累资源问题。最稳妥的写法是在回调里把每个case都包成“初始化-干活-终止”三个步骤,即使业务逻辑只有几行也是如此。

另外还要注意,uc1616回调里不要做耗时的同步操作。比如批量处理几百个对象这种事,如果直接在回调里做完,NX界面会卡住,用户体验很差。正确做法是把重活交给后台线程去跑,回调里只负责把任务启动起来。但后台线程里用UF_* API又涉及NX Open对不同线程的支持限制,没那么简单。所以更务实的建议是:功能是轻量级的(几十个对象以内的遍历、属性读写、简单参数修改)就直接在回调里做;功能一旦变重,老老实实引入进度条和合理的切分策略,别硬塞在回调里。

2.4 DLL编译链接时这几个配置不能搞错

uc1616要跑起来,工程配置里最核心的是“头文件路径、库文件路径、链接库列表、字符集、平台位数”这五件事。头文件和库文件都在NX安装目录的UGOPEN文件夹下,比如我装的是D盘Siemens NX 10.0,目录就是D:\Program Files\Siemens\NX 10.0\UGOPEN。附加包含目录指向这里,附加库目录也指向这里,链接依赖项加上libufun.lib和libugopenint.lib两个库——前者装uc系列函数,后者装UF_*系列函数。

字符集必须设置为“使用多字节字符集”。NX Open C API的函数参数基本都是char*类型,VS2013新工程默认是Unicode字符集,直接编译会报一堆类型不匹配的错误。平台位数也要和NX版本对齐,64位NX就必须编译x64位的DLL,用x86编译出来的DLL在64位进程里根本加载不上。很多新手的第一个“加载失败”就是这么来的。把关键配置列成一张表,照着检查就行:

配置项推荐值/路径
字符集使用多字节字符集
附加包含目录{NX安装目录}\UGOPEN
附加库目录{NX安装目录}\UGOPEN
附加依赖项libufun.lib;libugopenint.lib
平台x64(对应64位NX)

3. 实操过程与核心环节实现

3.1 VS2013工程创建与目录配置(以NX10.0为例)

我在NX10.0上习惯用VS2013来编DLL,原因很简单:NX10.0官方支持的就是VS2012和VS2013这两个编译器,VS2012太老,VS2013最常见。具体操作说一遍。

先打开VS2013,新建一个Win32项目,向导里选“下一步”而不是直接点完成,把应用程序类型设为DLL,勾上空项目,这样裸工程干净好配置。项目建好后,右键项目->属性,先看右上角配置和平台,把平台选为x64;没有x64就先在配置管理器里新建一个x64平台。然后逐项设置:

  • 通用属性->项目默认值->字符集:“使用多字节字符集”。
  • C/C++->常规->附加包含目录:填NX10.0的UGOPEN目录绝对路径。
  • 链接器->常规->附加库目录:同样填UGOPEN目录。
  • 链接器->输入->附加依赖项:加libufun.lib;libugopenint.lib。
  • C/C++->预处理器->预处理器定义:加_CRT_SECURE_NO_WARNINGS,用来屏蔽一些老API的编译警告。

如果你的NX10.0还安装了UGII下的其他开发库,比如要用到C++版的NXOpen接口,则还需要在附加包含目录里加NXOpen子目录,并在附加依赖项里加libnxopencpp.lib。但本文只做uc1616,不需要C++库,保持最小配置即可。配置完之后先别急着写代码,编译一个空的DLL验证工程本身没问题,能让后面的问题排查范围缩小一半。

3.2 一份可以直接抄的完整代码

下面这份代码是我整理过的最小模板,包含菜单注册、两个菜单项、回调分发、卸载设置四部分。把路径配置好以后,这份代码直接复制到main.cpp里编译能过。

#include <uf.h> #include <uf_object_types.h> #include <uf_exit.h> #include <ug_ufun.h> #define ITEM_ONE 1 #define ITEM_TWO 2 static int uc1616(int *response, int menu_id, int user_id); extern "C" __declspec(dllexport) void ufusr(char *param, int *returnCode, int rlen) { int menu_id = 0; int rc = 0; UF_initialize(); rc = uc1601("MyMenu", &menu_id, uc1616, &menu_id); if (rc != 0) { UF_terminate(); *returnCode = 1; return; } uc1602("Item One", menu_id, ITEM_ONE, 0); uc1602("Item Two", menu_id, ITEM_TWO, 0); UF_terminate(); *returnCode = 0; } extern "C" __declspec(dllexport) int ufusr_ask_unload(void) { return UF_UNLOAD_SEL_DIALOG; } static int uc1616(int *response, int menu_id, int user_id) { *response = 0; switch (user_id) { case ITEM_ONE: UF_initialize(); // 这里写第一个菜单项的功能 UF_terminate(); break; case ITEM_TWO: UF_initialize(); // 这里写第二个菜单项的功能 UF_terminate(); break; default: break; } return 0; }

代码的关键点有三个。第一,ufusr是DLL的入口,NX加载DLL后第一件事就是执行ufusr,菜单注册放在这里最合适。第二,uc1616被我定义成static函数,但不影响NX调用,因为我们通过uc1601把函数指针传出去了,地址是有效的;static可以避免这个回调符号被外部误引用,反而更干净。第三,ufusr_ask_unload返回UF_UNLOAD_SEL_DIALOG,这是保证DLL在NX会话期间不随便卸载的核心设置,等会儿细说。

如果编译时提示uc1601或uc1616未声明,多半是某种头文件没包含进来。不同NX版本的头文件名略有差异,到UGOPEN目录下翻一翻,找名字类似ug_ufun.h或ug_menu.h的文件,把对应的include加上。NXDOC里的官方示例很少直接用uc1616,多数是老的UG Open文档,所以遇到这种问题不要慌,以本机头文件为准。

3.3 加载运行:菜单怎么出来、回调怎么触发

代码编译通过后,在工程输出目录里会有一个MyDll.dll文件。打开NX10.0,新建或打开一个模型文件,然后点菜单File->Execute->NX Open,在文件选择框里选中刚编译的DLL,点确定。此时NX会执行ufusr,菜单栏里应该立刻多出一个“MyMenu”菜单。

点击MyMenu里的Item One,NX会调用uc1616,user_id等于ITEM_ONE,进入第一个case。怎么确认回调真的执行了?最简单的方法是临时在case里加一行UG日志输出:

UF_UI_open_listing_window(); UF_UI_write_listing_window("Item One callback hit\n");

注意用这两个函数需要包含uf_ui.h。重新编译DLL,再执行加载一次,点击菜单项,NX信息窗口里就会出现这行字。看到字,说明整个链路就跑通了。

这里有一个值得注意的加载行为:每次修改DLL代码后,需要先让NX卸载旧DLL,再加载新的。直接在Session里重复加载同一个DLL,NX可能保留旧模块,改的效果看不到。卸载的方式是在File->Execute->User Function相关功能里做,或者直接把NX关掉重开。实际开发时我一般直接重启NX,省心。

3.4 卸载模式:一个容易忽略的决定性细节

很多教程里的DLL例子都返回UF_UNLOAD_IMMEDIATELY,意思是ufusr执行完就立刻卸载。对“一次性任务”的程序(比如加载DLL后弹个对话框做点事就结束)没问题。但我们的uc1616菜单是要长期留在NX菜单栏里的,如果ufusr返回后DLL被立刻卸载,内存里的指令代码就没了,可NX还保留着菜单回调的函数指针,这时候点菜单会发生什么?轻则毫无反应,重则NX崩溃。

所以带菜单回调的DLL必须选择延迟卸载,返回UF_UNLOAD_SEL_DIALOG或UF_UNLOAD_TERMINATE。SEL_DIALOG的意思是NX在退出前或需要卸载时弹一个对话框询问用户;TERMINATE的意思是直到NX进程结束才允许卸载。作为模板,我习惯用SEL_DIALOG,这样调试时还能在NX的卸载交互里主动卸载DLL。如果用了IMMEDIATELY,菜单刷新一下就没了,典型的“刚才明明加载成功了,怎么菜单点了没反应”。

另外,如果用VS的调试器附加到NX进程里调试DLL,调试结束时不要强制“停止调试”就完事。先让NX正常退出,或者先卸载DLL,再停止调试,否则NX进程被中断,菜单注册信息可能残留,下一次启动会有各种奇怪状态。这条经验坑过我好几次,写在这里。

4. 常见问题与排查技巧实录

4.1 DLL加载失败的几个直接原因

我在网上看到最多的求助就是“File->Execute->NX Open选完DLL以后没反应”或“提示无法加载DLL”。这种事情九成以上出在下面几个地方。

一是位数不匹配。NX是64位进程,DLL必须是x64;你用Win32编译出来的DLL,加载时直接失败。到项目属性里看平台的配置即可。二是VC运行库缺失。VS2013编译的DLL依赖vc120运行库,有些精简版NX安装环境里没有装,加载就报“找不到msvcp120.dll”之类的错误。解决方法是把对应版本的Visual C++ Redistributable装上,或者把DLL改成静态链接运行库(在项目属性的C/C++->代码生成->运行库里选多线程//MT),后者更适合给公司内部多台电脑分发。

三是NX环境变量没配好。DLL运行时可能需要从UGII目录加载一些动态库,比如libufun.dll对应的运行支持。如果UGII_ROOT_DIR或者系统PATH里没有包含NX的UGII目录,加载就可能失败。NX正常安装后一般都会配好,但如果你搞过环境变量精简,要检查一下。四是链接库不对。libufun.lib和libugopenint.lib没链接,编译阶段就会报错,根本走不到加载阶段,所以这类问题一般在编译阶段就会被卡住,反而容易发现。把常见现象汇总到这里:

现象大概率原因快速定位方法
提示加载失败/没反应32/64位不匹配检查项目平台
缺msvcp120.dll等运行库VC运行库缺失装VC++运行库或静态链接/MT
找不到相关NX动态库UGII目录不在PATH检查环境变量
加载后菜单栏无变化菜单标题冲突或DLL被卸载换标题、检查卸载模式

4.2 菜单加载了但不显示回调不触发

DLL加载成功但菜单栏里看不到新菜单,或者菜单在但点了没反应,这种问题比较隐蔽。先取消“菜单标题冲突”这个嫌疑:如果之前曾经用同样的标题注册过菜单,而旧DLL还在NX进程里,新加载的DLL注册同名菜单可能会失败。解决方法是换一个标题测试,或者清理卸载旧DLL后再注册。

假如菜单在,但点击没进回调,优先怀疑DLL已经被卸载。检查ufusr_ask_unload的返回值是不是UF_UNLOAD_IMMEDIATELY。如果是,就是前面说的卸载问题。另外还有一种情况:DLL虽然返回SEL_DIALOG,但NX加载时弹出了卸载询问对话框,被误点成“卸载”,菜单在但回调函数指针已经失效。遇到这种迷糊情况,重启NX重新加载一次就好。

还有一种比较隐蔽的:DLL里的uc1616定义成了某个成员函数或加了错误的函数签名,导致函数指针类型不匹配。uc1601注册回调时编译会对函数指针做类型检查,类型不对会编译报错,一般来不到运行阶段。如果你是用C++项目但没加合适的extern"C"处理,也有可能在某些编译器配置下出问题。最省事的是照着模板定义成普通C函数,不放在类里。

4.3 回调进去了,一干活就崩溃

回调能被触发,说明基础链路没问题。一旦在回调里调用具体功能就崩溃,往往是三件事。第一是UF_initialize/UF_terminate没有成对出现,或调用顺序有误。在UF_initialize之前就调UF_*系列函数,NX会直接异常;在UF_terminate之后再做UF_*调用,也一样。把case里的逻辑检查一遍,确保初始化在最前,释放在最后。

第二是访问了空指针或持有过期的对象Tag。比如你事先缓存了一个部件或用例的Tag,再去使用时部件已经关闭,这类Tag在下次会话里是无效的。回调属于事件驱动的运行环境,不能用全局变量保存NX对象的Tag长期复用。稳妥的做法是每次回调进来时,通过UF_ask_*系列函数现场获取当前显示部件、当前工作部件再用。

第三是混合了C API和C++ API时生命周期没管理好。uc1616是C风格的API,如果项目同时链接了NXOpen C++库,在C回调里获取C++对象后要注意释放。很多老工程师的原则是“一个DLL尽量只用一套API”,要么走UC/UF纯C接口,要么整体搬到NXOpen C++,混着用调试成本倍增。

4.4 我个人常用的三招排查手法

第一招,写日志。在uc1616第一行就写文件日志,记录进入回调时的menu_id和user_id。如果在日志里能看到这两个值,说明回调通了;看不到,问题在菜单注册或加载阶段。日志比断点更可靠,因为断点可能需要附加调试器,附加上去时NX的某些线程状态可能变化,反而不容易复现。

第二招,VS附加进程调试。以管理员身份启动VS2013,在uc1616里下一个断点,然后菜单“调试->附加到进程”,选中nx.exe进程。回到NX里点击菜单项,断点就会命中。这一步能看到真实的调用栈、变量值和函数参数,排查效率极高。附加调试需要NX和VS以相同版本和位数运行,否则符号加载会有问题。

第三招,用依赖检查工具看DLL依赖。当年我用Dependency Walker查过好几次这种老式DLL,一查就能看出少了哪个运行库、哪个系统DLL对不上。现在Windows下可以用微软的dumpbin命令快速看DLL依赖,在VS开发命令行里执行:

dumpbin /dependents MyDll.dll

如果看到没有名的DLL带问号,就去安全模式下判定该补什么运行库。

5. 项目扩展与个人体会

5.1 把uc1616回调接上实际业务

模板通了之后,接实际业务就方便了。比如给第二个菜单项写一个“导出当前部件所有属性到CSV”的功能,在case ITEM_TWO里先用UF_ask_current_part拿到当前显示部件Tag,再遍历属性链直到为空,最后用C标准库写文件。整个过程差不多五十行代码,uc1616只负责把它挂到菜单上。

case ITEM_TWO: UF_initialize(); { tag_t part_tag = NULL_TAG; UF_ask_current_part(&part_tag); if (part_tag != NULL_TAG) { // 遍历属性、导出CSV } } UF_terminate(); break;

还有一个很实用的变体:同一个DLL里注册多条菜单。用uc1601分别注册两个菜单,各用不同的menu_id,回调里先判断menu_id再判断user_id,这样可以把“建模辅助”“检查工具”分门别类放好,避免一个菜单项列表长得没边。菜单之间回调依然是同一个uc1616也无所谓,反正有menu_id做区分。

5.2 新项目要不要还用uc1616

如果是新项目、从零开始,我会推荐考虑NXOpen C++和Ribbon菜单,毕竟NX11以后官方把大量精力放在新界面上,老式菜单在高版本上的测试覆盖会越来越少。但如果是NX10.0环境下的内部工具、或者需要快速交付的小功能,uc1616依然是非常高效的起点。记住这个判断标准:功能复杂度低、界面要求不高、交付速度快优先,就用老方案;界面复杂、交互多、要长期迭代,直接上NXOpen C++。

5.3 我的一点实际操作体会

这套东西我从NX6一路用到NX10.0,最大的体会是“别看它老,稳定才是硬道理”。uc1616的机制几十年没变过,各种踩坑经验在网上可以搜到一大堆,遇到问题不怕没参考。而且这套思路对理解NX Open体系很有帮助:ufusr是入口,回调是事件,初始化是环境,卸载是生命周期——这套四段论在任何NX开发方式里都成立。先把这套老路子吃透,再去啃NXOpen C++,你会发现框架其实是共通的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 17:29:47

WorkBuddy+Obsidian:公式教材与自动出题工作流

1. 培训教材生产的真实困境与破局思路做培训讲师这些年&#xff0c;最头疼的从来不是站在讲台上讲课&#xff0c;而是台下那些看不见的准备工作。尤其是理工科方向的培训&#xff0c;公式教材的编写和配套习题的出题&#xff0c;几乎占据了我60%以上的备课时间。一份30页的公式…

作者头像 李华
网站建设 2026/10/6 17:28:00

OpenShell:终端里的AI聊天界面,聚合Ollama与OpenAI兼容服务

做命令行工具久了&#xff0c;我有个很深的感受&#xff1a;圈子里从来不缺好用的AI助手&#xff0c;缺的是能把这些助手“收拢”到一起的界面。今天想聊的开源项目OpenShell&#xff0c;就是干这件事的。它基于Python的Textual框架做了一套终端聊天界面&#xff0c;把shell-gp…

作者头像 李华
网站建设 2026/10/6 17:25:29

商业计划书深度构建与表达策略:从想法到决策

我一直觉得&#xff0c;商业计划书是国内创业生态里被误解最深的一份文档。很多人以为它是写给投资人看的申请书&#xff0c;实际上它更像一张决策图纸——用最短的时间、最清晰的方式&#xff0c;让一个冷静的陌生人愿意为你押上注意力和筹码。我见过太多项目&#xff0c;产品…

作者头像 李华
网站建设 2026/10/6 17:23:45

仿银行系统开发实战:数据模型、事务与并发控制全解析

简介&#xff1a;这是一套仿银行系统的C# WinForm工程源码&#xff0c;面向有一定基础或初学C#的开发者&#xff0c;适用于课程设计、毕业设计&#xff0c;也可用于快速理解银行存取款、转账、账户管理等核心业务的系统实现。压缩包共44个文件&#xff0c;主体为11个C#源文件&a…

作者头像 李华
网站建设 2026/10/6 17:23:30

OpenHarmony Flutter工程import_rules依赖控制

上个月我梳理一个 OpenHarmony 平板上的 Flutter 工程时&#xff0c;被 dart analyze 的报错清单吓了一跳&#xff1a;presentation 层的页面直接 import 了 data 层的 Repository 实现类&#xff0c;domain 层的接口和 data 层的 DTO 互相引用&#xff0c;core 层里不知道什…

作者头像 李华
网站建设 2026/10/6 17:22:06

Unity AR涂色开发实战:从图像识别到Shader合成与导出

简介&#xff1a;这份资源面向Unity开发者与AR互动应用爱好者&#xff0c;聚焦增强现实与实时涂色结合的实践方案&#xff0c;帮助读者理解如何借助EasyAR等插件完成图像识别、目标跟踪与虚拟上色&#xff0c;适合具备一定Unity基础、希望切入AR互动娱乐场景的中级开发者。压缩…

作者头像 李华