-
前言不久前在仓颉的交流群中看到了关于仓颉案例共创的活动消息,于是想到可以用仓颉结合自身所长开发一个AI桌面宠物供大家娱乐学习。正好笔者封装的仓颉桌面渲染库 RGF_CJ 和控件库 RGF_UI 能满足这方面的需求,那么说干就干!成品效果开发前的准备在正式进行应用的开发之前,需要先准备好相关的依赖。领取华为云DeepSeek免费资源华为云为开发者提供了单模型200万Tokens的免费额度,包含DeepSeek-R1&V3满血版,登录华为云ModelArts Studio(MaaS)控制台即可领取,此处可以选择DeepSeek-R1满血版来搭建AI聊天桌宠。提示:点击此链接领取:https://www.huaweicloud.com/product/modelarts/studio.html进入网站签署服务声明领取免费额度并查看调用说明通过说明中的链接去创建API点击按钮创建API填写API相关信息获取API并保存提示:请保存好此处的API链接,后续代码开发中会用到。获取RGF依赖文件在后续应用的开发中会依赖到RGF_UI库,此库依赖到一些文件,可以从RGF_UI的库中获取。这些依赖文件后续会链接入应用中去。提示:点击此链接下载:https://gitcode.com/raozj/RGF_UI/tree/master/libs进入RGF_UI库的Libs目录,下载依赖文件保存依赖文件,留待后续使用安装仓颉STDX及相关依赖因为是基于仓颉开发通信相关的应用,因此需要依赖到STDX中的stdx.net.http、stdx.encoding.json、stdx.net.tls等包,而这些包自Cangjie v0.60.5版本开始从std标准库中移出,因此需要自行安装这些包以及这些包所依赖的OpenSSL。提示:点击此链接下载:https://gitcode.com/Cangjie/Cangjie-STDX注’ 由于笔者发文时Cangjie v0.60.5版本尚且处于内测阶段,因此若开发者无内测资格则无法下载到此版本的相关文件。关于 STDX 和 OpenSSL 的安装请参考上述链接中官方的文档,此处不做赘述。序列帧合图资源制作作为一个桌面宠物,自然需要拥有对应的动画,对此可以使用DragonBones、Spine或者Adobe Effect等软件制作。制作动画笔者在此使用的是DragonBones,关于DragonBones的安装和使用不在本文的探讨范畴之内,读者可自行选择喜欢的动画制作软件,设计并制作完成动画后输出序列帧以用于应用的开发。设计动画制作出序列帧制作合图资源如果每帧的图片都单独存储,对于应用的初始化加载和渲染都是一个较大的负担,因此建议将一类的序列帧动画按照相等宽高、间距合并为一张图片。由于手动合图比较麻烦,因此笔者写了一个Python脚本,快速的将目录中的序列帧图片按照指定的宽度分布数量合并为一张图片。注’ 由于此部分不属于文章核心部分,因此直接贴出代码以供参考,读者可通过其它工具或代码实现自己个性化的合图方案。import os import cv2 import numpy as np def load_images_from_folder(folder): """ 加载指定文件夹下的所有 .png 图像 """ image_files = [f for f in os.listdir(folder) if f.lower().endswith('.png')] image_files.sort() # 按文件名排序 images = [] for filename in image_files: file_path = os.path.join(folder, filename) # 读取图像(支持中文路径) with open(file_path, 'rb') as f: img_data = np.frombuffer(f.read(), dtype=np.uint8) img = cv2.imdecode(img_data, cv2.IMREAD_UNCHANGED) images.append(img) print(f"已加载: {filename}") return images def stitch_images(images, cols, bg_color=(0, 0, 0, 0)): """ 将图像按指定列数拼接成一张大图 :param images: 图像列表 :param cols: 每行显示的图像数量 :param bg_color: 背景颜色(支持 RGBA) :return: 拼接后的图像 """ if not images: raise ValueError("没有图像可供拼接") h, w = images[0].shape[:2] rows = (len(images) + cols - 1) // cols # 创建空白背景图 channels = images[0].shape[2] if len(images[0].shape) == 3 else 1 if channels == 1: bg_color = (bg_color[0], ) # 灰度图 elif channels == 3: bg_color = bg_color[:3] # RGB elif channels == 4: bg_color = bg_color[:4] # RGBA canvas = np.full((h * rows, w * cols, channels), bg_color, dtype=np.uint8) for idx, img in enumerate(images): row = idx // cols col = idx % cols x = col * w y = row * h canvas[y:y+h, x:x+w] = img return canvas def main(): folder = input("请输入包含PNG图像的文件夹路径:").strip() if not os.path.isdir(folder): print("输入的不是有效文件夹路径!") return try: cols = int(input("请输入每行显示的图像数量:")) except ValueError: print("请输入有效的数字!") return output_path = input("请输入输出图像的完整路径(如 output.png):").strip() images = load_images_from_folder(folder) if not images: print("未找到任何PNG图像!") return # 检查所有图像是否尺寸一致 first_h, first_w = images[0].shape[:2] for img in images[1:]: h, w = img.shape[:2] if h != first_h or w != first_w: print("警告:图像尺寸不一致,可能影响拼接效果。") # 拼接图像 print("开始拼接图像...") stitched_image = stitch_images(images, cols) # 保存结果 cv2.imwrite(output_path, stitched_image) print(f"拼接完成,已保存为: {output_path}") if __name__ == '__main__': main() 开发AI桌宠基本规划首先对项目目录和文件进行基本的规划,后续的开发就基于规划进行。目录规划Yez/ -------------------------------------------# 项目主目录 ├─libs/ ---------------------------------------# 依赖库 ├─res/ ----------------------------------------# 资源 │ └─SequenceFrame/ ---------------------------# 序列帧资源 │ └─skip_white/ ---------------------------# 椰椰的白色皮肤资源 ├─src/ ----------------------------------------# 源代码目录 └─tool/ ---------------------------------------# 工具目录文件规划Yez/ ├┈cjpm.lock ├┈cjpm.toml ├─libs/ │ ├┈libimm32.a -------------------------------# Windows 输入法库 │ ├┈resources.rc.o ---------------------------# 资源配置清单 内部声明应用依赖的最低系统版本 │ └┈libRgf.dll -------------------------------# RGF渲染库依赖的动态库 ├─res/ │ ├┈config.json ------------------------------# 椰椰的配置文件 │ └─SequenceFrame/ │ └─skip_white/ │ ├┈blink.json -------------------------# 眨眼合图描述文件 │ ├┈blink.png --------------------------# 眨眼动画合图 │ ├┈jump.json --------------------------# 跳跃合图描述文件 │ ├┈jump.png ---------------------------# 跳跃动画合图 │ ├┈speak.json -------------------------# 说话合图描述文件 │ ├┈speak.png --------------------------# 说话动画合图 │ ├┈wait.json --------------------------# 待机合图描述文件 │ └┈wait.png ---------------------------# 待机动画合图 ├─src/ │ ├┈chat.cj ----------------------------------# 大模型网络通信代码 │ ├┈config.cj --------------------------------# 配置管理代码 │ ├┈main.cj ----------------------------------# 程序主入口代码 │ ├┈res.cj -----------------------------------# 资源管理代码 │ ├┈win_anm.cj -------------------------------# 动画绘制窗口代码 │ ├┈win_input.cj -----------------------------# 输入框窗口代码 │ └┈win_text.cj ------------------------------# 鼠标文本渲染代码 └─tool/ └┈picture_puzzle.py ------------------------# python 合图脚本代码链接文件与库配置在开发之前,需要现为当前项目链接依赖文件和添加依赖库。链接文件在之前的小节中,下载了RGF_UI的依赖文件压缩包,在下载的压缩包内应该包含三个文件,即libRgf.dll、resources.rc.o和libimm32.a。这三个文件中,libRgf.dll是RGF_CJ库的依赖文件,必须随应用打包发布;resources.rc.o文件是Windows资源描述文件,读者也可以自行编写resources.rc文件并生成此文件(需设置最低支持Windows 8,即内核NT 6.2);libimm32.a文件是自绘编辑框控件依赖的文件,主要提供输入法相关的操作能力。在仓颉包管理配置toml文件中,需要加入链接上述文件的代码:[target] [target.x86_64-w64-mingw32] link-option = "-L ./libs -l:libRgf.dll -l user32 -l imm32" 库配置完成链接后,还需要为应用声明依赖RGF_CJ和RGF_UI包,并且为RGF_CJ包配置宏。提示:点击此链接查看依赖库RGF_CJ:https://gitcode.com/Cangjie-SIG/RGF_CJ提示:点击此链接查看依赖库RGF_UI:https://gitcode.com/raozj/RGF_UI[dependencies] [dependencies.rgf] git = "https://gitcode.com/Cangjie-SIG/RGF_CJ.git" [dependencies.rgfui] git = "https://gitcode.com/raozj/RGF_UI.git" [profile] [profile.customized-option] rgfCfg = "--cfg \"RGF_LANG=zh-cn\"" 上述代码中配置的“RGF_LANG=zh-cn”表示:当RGF_CJ出现异常时,捕获输出的异常提示内容为中文。配置加载模块笔者希望桌宠能够具备记忆配置的能力,因此需要为桌宠写一个配置的加载、保存模块,这个模块会记住桌宠所使用的皮肤、最后所在的桌面位置等信息,以确保桌宠重启后依然符合之前的情况。编写一个配置管理相关的恶汉单例类ConfigMgr,单例具备两个方法,一个用于从文件中加载配置,一个用于写出配置到文件中去。(其实也可以直接编写静态方法或直接编写函数而不使用类,看大家自己的选择吧。)代码如下:// Yez/src/config.cj // 配置管理类 class ConfigMgr { private static let _instance:ConfigMgr = ConfigMgr() public static prop i:ConfigMgr{ get(){ _instance } } private init() {} ... /** * 加载配置文件 * @return 是否成功 */ public func readConfig():Bool{ ... } /** 写出配置文件 */ public func writeConfig():Unit{ ... } ... } 编写一个ConfigInfo类,这个类用于存放实际的配置项,并且提供转换为Json的能力,代码如下:// Yez/src/config.cj class ConfigInfo <: Serializable<ConfigInfo> { // 椰椰右下角窗口坐标系的X坐标 var _x:Int32 = Int32.Min // 椰椰右下角窗口坐标系的Y坐标 var _y:Int32 = Int32.Min // 椰椰使用的皮肤 var _skip = "skip_white" // 椰椰画面缩放比例 var _zoom:Float32 = 0.125 // 椰椰的字体大小 var _fontSize:Float32 = 16.1 public func serialize(): DataModel { return DataModelStruct().add(field<Int32>("x", _x)) .add(field<Int32>("y", _y)) .add(field<String>("skip", _skip)) .add(field<Float32>("zoom", _zoom)) .add(field<Float32>("fontSize", _fontSize)) } public static func deserialize(dm: DataModel): ConfigInfo { var dms = match (dm) { case data: DataModelStruct => data case _ => throw Exception("this data is not DataModelStruct") } var result = ConfigInfo() result._x = Int32.deserialize(dms.get("x")) result._y = Int32.deserialize(dms.get("y")) result._skip = String.deserialize(dms.get("skip")) result._zoom = Float32.deserialize(dms.get("zoom")) result._fontSize = Float32.deserialize(dms.get("fontSize")) return result } } 将ConfigInfo以成员变量的形式加入到ConfigMgr中,并且由ConfigMgr对外提供访问属性,代码如下:// Yez/src/config.cj // 配置管理类 class ConfigMgr { // 配置信息 private var configImpl:ConfigInfo = ConfigInfo() ... public mut prop x:Int32{ get(){ return configImpl._x } set(val){ configImpl._x = val } } public mut prop y:Int32{ get(){ return configImpl._y } set(val){ configImpl._y = val } } public mut prop skip:String{ get(){ return configImpl._skip } set(val){ configImpl._skip = val } } public prop zoom:Float32{ get(){ return configImpl._zoom } } public prop fontSize:Float32{ get(){ return configImpl._fontSize } } } 接下来只要完善readConfig和writeConfig的读取和写出逻辑即可,代码如下:// Yez/src/config.cj // 配置文件路径 const CONFIG_FILE_PATH = "./res/config.json" ... public func readConfig():Bool{ // 判断目录是否存在 if(exists(CONFIG_FILE_PATH)){ let seqFramePath = canonicalize(CONFIG_FILE_PATH) let bytes = File.readFrom(seqFramePath) var jv = JsonValue.fromStr(StringReader(ByteBuffer(bytes)).readToEnd()) var dm = DataModel.fromJson(jv) configImpl = ConfigInfo.deserialize(dm) } // 目前始终返回 true // 当配置不存在时,则使用默认配置 return true } public func writeConfig():Unit{ if(!exists(CONFIG_FILE_PATH)){ File.create(CONFIG_FILE_PATH) } let seqFramePath = canonicalize(CONFIG_FILE_PATH) var Anm = configImpl.serialize() var dm = Anm.toJson() File.writeTo(seqFramePath,dm.toString().toArray()) } 最后,在入口函数中启用配置加载与写出即可。// Yez/src/main.cj main(): Int64 { // 加载配置文件数据 if(!ConfigMgr.i.readConfig()){ return 0 } ... // 保存配置到文件中去 ConfigMgr.i.writeConfig() return 0 } 资源加载模块除了配置之外,应用资源也是一个重要的部分,这部分的完整性会影响到应用是否正常渲染。首先采用和Config部分类似的结构准备恶汉单例类ResMgr和动画信息类AnmInfo,代码如下:// Yez/src/res.cj // 资源管理类 class ResMgr { private static let _instance:ResMgr = ResMgr() public static prop i:ResMgr{ get(){ _instance } } private init() {} // 动画 private static const ANM_BLINK ="blink" private static const ANM_JUMP ="jump" private static const ANM_SPEAK ="speak" private static const ANM_WAIT ="wait" private let _skipImg:HashMap<String, Array<(AnmInfo,MemBitmap)>> = HashMap<String, Array<(AnmInfo,MemBitmap)>>() public func loadRes():Bool{ ... } public func unloadRes(){ ... } public prop skip:HashMap<String, Array<(AnmInfo,MemBitmap)>>{ get(){ return _skipImg } } } class AnmInfo <: Serializable<AnmInfo> { // 单帧图片宽度 var width:Int32 = 0 // 单帧图片高度 var height:Int32 = 0 // 合图图片每行图片数量 var hCount:Int32 = 0 // 总帧数 var frames:Int32 = 0 ... } 然后就是实现在loadRes方法中加载合图及合图相关配置文件,代码逻辑还就是:从"./res/SequenceFrame"目录开始确保目录存在,然后遍历SequenceFrame目录下的所有皮肤目录,并且将皮肤目录内的合图资源及对应的配置文件载入到成员中去。如果目录下的资源文件不完整,则不会加载对应的内容到成员中。// Yez/src/res.cj /** * 代码较为复杂且不是核心代码,读者了解功能即可。 * 若需要查看相关代码,可下载项目查看。 */ public func loadRes():Bool{ ... } public func unloadRes(){ //释放所有图像资源(设备无关资源) for(skipPath in _skipImg){ skipPath[1][0][1].release() skipPath[1][1][1].release() skipPath[1][2][1].release() skipPath[1][3][1].release() } } 最后,在入口函数中启用资源加载与释放即可。// Yez/src/main.cj main(): Int64 { ... // 加载依赖资源 if(ResMgr.i.loadRes()){ ... } // 释放一切可能被加载的资源 ResMgr.i.unloadRes() ... return 0 } 大模型模块接下来便是与AI通信的核心部分了,此部分需要完成与大模型的互通。我们需要准备一个LLM类,这个类负责基于API、URL等信息与大模型建立链接,并且解析信息。// Yez/src/chat.cj class LLM { let client: Client let history = StringBuilder() public LLM( // 大模型URL链接 let url!: String, // 大模型 API Key let key!: String, // 大模型类型标识 let model!: String, // 上下文 let context!: Bool = false ) { var config = TlsClientConfig() config.verifyMode = TrustAll client = ClientBuilder() .tlsConfig(config) // AI 服务响应有时候比较慢,这里设置为无限等待 .readTimeout(Duration.Max) .build() } /** * 编码函数,将角色与内容编码为字符串 * return 字符串 */ func encode(role: Role, content: String) { ... } /** * 发送函数,将对话数据发送到服务器 * return HttpResponse */ func send(input: String, stream!: Bool = false) { ... } /** * 分析函数,解析服务器返回的数据并且提取其中有效的文本内容 * return 有效内容 */ func parse(text: String, stream!: Bool = false) { ... } /** * 流式对话 * @param input 输入的文本 * @param task 流式返回内容的回调函数 * @param taskEnd 返回内容结束的回调函数 */ public func chat(input: String, task!: (String) -> Unit = {o => print(o)},taskEnd!:()->Unit = {=>}) { ... } /** * 非流式对话 */ public func chat(input!: String) { ... } /** * 历史记忆预置 */ public func preset(context: Array<(Role, String)>) { ... } /** * 历史记忆清除 */ public func reset() { ... } } 根据华为云提供的调用参考,完善 send 方法代码如下:// Yez/src/chat.cj func send(input: String, stream!: Bool = false) { let message = encode(I, input) let content = '{"model":"${model}","messages":[${history}${message}],"stream":${stream}}' if (context) { history.append(message) } let request = HttpRequestBuilder() .url(url) .header('Authorization', 'Bearer ${key}') .header('Content-Type', 'application/json') .header('Accept', if (stream) { 'text/event-stream' } else { 'application/json' }) .body(content) .post() .build() client.send(request) } 根据华为云返回的数据格式,完善 parse 方法代码如下:// Yez/src/chat.cj func parse(text: String, stream!: Bool = false) { let json = JsonValue.fromStr(text).asObject() let choices = json.getFields()['choices'].asArray() if(choices.size() == 0){ return "" } // 流式和非流式情况下,这个字段名称不同 let key = if (stream) { 'delta' } else { 'message' } let message = choices[0].asObject().getFields()[key].asObject() if(message.containsKey("content")){ let content = message.getFields()['content'].asString().getValue() // 移除开头的两个 \n if(content == "\n\n"){ return "" } return content }else{ return "" } } 最后实现核心的流式对话部分,代码如下:// Yez/src/chat.cj public func chat(input: String, task!: (String) -> Unit = {o => print(o)},taskEnd!:()->Unit = {=>}) { // 根据传入的字符串构建上发服务器的 HttpResponse let response = send(input, stream: true) // 准备一个字符串缓冲区 let output = StringBuilder() // 准备一个字节缓冲区 let buffer = Array<Byte>(1024 * 8, {i=>0}) // 获取返回的内容 var length = response.body.read(buffer) while (length != 0) { // 将字节缓冲区内的内容按照UTF-8编码解析为字符串 let text = String.fromUtf8(buffer[..length]) const INDEX = 6 // 按行分割 for (line in text.split('\n', removeEmpty: true)) { // 确保获取的是完整的:data: {...} if (line.size > INDEX && line[INDEX] == b'{' && line[line.size - 1] == b'}') { let json = line[INDEX..line.size] // 分析出有效信息 let slice = parse(json, stream: true) if (context) { output.append(slice) } // 若有效信息不为空,则通过回调函数返回 if(slice != ""){ task(slice) } } } length = response.body.read(buffer) } if (context) { history.append(',${encode(AI, output.toString())},') } // 所有处理结束后,执行结束回调 taskEnd() } LLM类是一个较为通用的类,但是使用较为繁琐,因此对于外部调用而言,只需要一个简化的实例即可。因此,可以再编写一个独立的懒汉单例LLMImpl类,并且仅提供一个对外实现流式对话的chat方法,代码如下:// Yez/src/chat.cj class LLMImpl { private static var _i: Option<LLMImpl> = None public static prop i:LLMImpl { get(){ if(_i.isNone()){ _i = LLMImpl() } return _i.getOrThrow() } } private let robot:LLM // 初始化一个对话模型 private init(){ robot = LLM( // 华为云大模型调用URL url: 'https://maas-cn-southwest-2.modelarts-maas.com/v1/infers/8a062fd4-7367-4ab4-a936-5eeb8fb821c4/v1/chat/completions', // 如果示例自带的密钥失效,请自行注册,https://www.huaweicloud.com/product/modelarts/studio.html key: '/* !!! 这里填写读者自己的 API Key !!! */', // 要使用的 大模型 model: 'DeepSeek-R1', context: true ); // 设定基本信息 robot.preset([(System, #"我会自称“椰椰”并亲昵的在三句话之内回复所有问题。对于限定长度内无法阐述清晰的问题,我会表现出呆傻可爱的效果并说出“宕机”。如果对我说“拍照”或“截图”,我就会说“咔嚓~”。如果问我是谁开发的,我会说出“尧佥&椰子”。"#)]) } /** * 流式对话 * @param content 输入的文本 * @param task 流式返回内容的回调函数 * @param end 返回内容结束的回调函数 */ public func chat(content:String, task!: (String) -> Unit = {o => print(o)},end!:()->Unit = {=>}){ robot.chat(content,task:task,taskEnd:end) } } 注’ 完整的代码请参考提供的代码文件桌宠渲染接下来,需要实现可视化的窗口渲染,赋予“椰椰”一副“身体”。动画播放窗口为了实现AI桌宠的渲染,首先需要有一个窗口。对此,笔者选择基于RGF_CJ中的WinBase类,实现一个自己的窗口类WinMain。// Yez/src/win_anm.cj // 使用 RGF_CJ 的宏实现代码简化 @LifeCycle // 自动管理 设备相关资源 的释放 @HookProc // 自动挂载窗口类的消息事件方法 class WinMain <: WinBase{ ... // 窗口类初始化时,传入 ResMgr 载入的资源(设备无关资源) init(anm:Array<(AnmInfo, MemBitmap)>){ _anmRes = anm } /** * 加载设备无关资源 -> 设备相关资源 * @note 本部分将资源ResMgr类中的图片资源,载入到窗口类中 */ private func loadAnm(anm:Array<(AnmInfo, MemBitmap)>){ surface .createBitmapFromMemory(_bmps[0],anm[0][1]) .createBitmapFromMemory(_bmps[1],anm[1][1]) .createBitmapFromMemory(_bmps[2],anm[2][1]) .createBitmapFromMemory(_bmps[3],anm[3][1]) } /** * 创建设备资源事件,窗口渲染表面创建完成后,开始载入资源 */ public func createDeviceResources():Bool{ // 这里调用 loadAnm 载入图片资源 loadAnm(_anmRes) return true; } /** * 窗口创建完成事件,当窗口创建完成时,开始渲染动画 */ public func created(hwnd:RgfHwnd,x:Int32, y:Int32, nWidth:Int32, nHeight:Int32):Unit{ ... } /** * 绘制窗口事件,此处根据当前的动画状态渲染窗口 */ public override func onPaint(wRect:Rect):Unit{ surface.clear(0.0,0.0,0.0,0.0) match(_nowState){ case Wait => {=> // 等待状态的动画渲染比较特殊,因为需要实现眼睛跟随鼠标移动 if(_bmps[3].isValid() && _bmps[0].isValid()){ // 这一部分计算当前渲染帧的静态部分,即:身体, // 并且将这部分内容渲染到窗口 .... // 然后下面计算出鼠标对眼睛造成的偏移量 // 并且将眼睛渲染到身体上 if(_anmStep < _anmRes[0][0].frames){ .... }else{ .... } } }() case Jump => {=> // 这里渲染椰椰的跳跃动画 if(_bmps[1].isValid()){ .... } }() case Speak => {=> // 这里渲染椰椰正在说话的动画 if(_bmps[2].isValid()){ .... } }() } } } 在入口函数中,启用UIMain并且创建窗口,代码如下:// Yez/src/win_anm.cj main(): Int64 { ... if(ResMgr.i.loadRes()){ // 开始创建UI UIMain{=> // 确保皮肤配置有效 if(!ResMgr.i.skip.contains(ConfigMgr.i.skip)){ ConfigMgr.i.skip = ResMgr.i.skip.keys().toArray()[0] } // 创建窗口类 let anmWin:WinMain = WinMain(ResMgr.i.skip.get(ConfigMgr.i.skip).getOrThrow()) // 先随便在桌面 (0,0) 的位置创建一个 10像素×10像素 的窗口 // 窗口类内部会在显示前根据配置中的信息自动布局到正确位置和尺寸 anmWin.createWin(uiGetContext(), "Yez", 0, 0, 10, 10, 0, WinStyle.e2n(WinStyle.Popup), // 使用的是 TopMost 分层窗口(支持透明背景) dwExStyle:WinStyleEx.Default | WinStyleEx.TopMost, // 渲染使用的是 Direct2D 计数 layered:true,substrate:Substrate.Direct2D ) } } ... } 完成上述代码后,窗口已经可以正确的渲染动画了注’ 完整的代码请参考提供的代码文件编辑框窗口对于编辑框的开发是比较繁琐的,因此选择直接继承RGF_UI的UIInput类,然后将此控件修改为窗口,首先需要创建一个编辑框窗口类InputWin,代码如下:// Yez/src/win_input.cj public open class InputWin <: UIInput{ // 为编辑框设置独立的UI样式,而不是继承全局的样式 let css:UITheme = UITheme() init(){ super(theme:css) // 从配置中获取字体大小 css.fontSizeM = ConfigMgr.i.fontSize // 更新样式配置 css.update() } /** * 获取输入框窗口预期出现的位置 * @return 编辑框窗口坐标 * @note 本部分计算编辑框在当前椰椰所在桌面中心时的位置 */ private func getTargetLocation():Point{ // 方案二、桌面中心 let monitor = rsGetMonitorFromPoint(rsGetCursorPos()) let pos = Point( ((monitor.right - monitor.left) - INPUT_WIN_WIDTH) / 2, ((monitor.bottom - monitor.top) - INPUT_WIN_HEIGHT) / 2 ) return pos } /** * 创建窗口 */ public func createWin(hwndParent:RgfHwnd):RgfHwnd{ // 获取出现位置坐标 let pos = getTargetLocation() // 创建一个置顶层的无边框编辑框窗口 let retv = super.createWin( uiGetContext(),"",pos.x,pos.y,INPUT_WIN_WIDTH,INPUT_WIN_HEIGHT,hwndParent, WinStyle.PopupWindow | WinStyle.ClipChildren, dwExStyle:WinStyleEx.Default | WinStyleEx.TopMost, substrate:Substrate.Direct2D,layered:false,bgMod:BackGroundMod.CoverNotDraw,drawInterval:0 ) // 设置默认的内容为:“想对椰椰说什么:” content = INPUT_PROMPT // 创建好就显示窗口 show(true) return retv } } 完成编辑框窗口的创建后,还需要能响应相关的输入事件。对此,笔者希望:a) 当编辑框被按下或输入内容时,如果内容为默认内容,则清空提示内容;b) 当编辑框失去焦点时,窗口主动销毁自身;c) 当编辑框按下回车键时,将编辑框内容上发给大模型,并开始处理返回消息。实现自动清空内容很简单,代码如下:// Yez/src/win_input.cj /** 鼠标左键按下时,判断内容并清空 */ public override open func onLButtonDown(e:EvMouse):Unit{ if(content == INPUT_PROMPT){ content = "" } super.onLButtonDown(e) } /** 键盘按下某键时,判断内容并清空 */ public override open func onKeyDown(e:EvKey):Unit{ if(content == INPUT_PROMPT){ content = "" } super.onKeyDown(e) } 得益于RGF_CJ库的安全性,窗口销毁会自动释放资源,避免了繁琐的资源释放,和释放时机的管理。实现窗口失去焦点就是否窗口的代码如下:// Yez/src/win_input.cj /** 窗口失去焦点时,销毁窗口并释放窗口资源 */ public override open func onKillFocus(e:EvKillFocus):Unit{ destroyWin() } 希望实现按下回车键触发对话,则需要在对应的事件中调用LLMImpl类的流式对话方法,代码如下:// Yez/src/win_input.cj /** 字符输入事件 */ public override open func onChar(e:EvChar):Unit{ // 判断输入的是否为回车键 if(e.char == 0x0D/*VK_RETURN*/){ // 是回车键就销毁本窗口 destroyWin() // 并且调用 开始回答函数 start() // 然后调用 LLMImpl 类的流式对话方法 // 并且传入 流的回调函数 以及 回答结束的函数 LLMImpl.i.chat(content,task:task,end:end) }else{ super.onChar(e) } } // 回复流 public var task:(String) -> Unit = {str=>} // 开始回答 public var start:() -> Unit = {=>} // 开始回答 public var end:() -> Unit = {=>} 编辑框控件的效果如下图所示:注’ 完整的代码请参考提供的代码文件反馈窗口完成上述内容后,输入与处理已经基本完备,接下来需要实现文本的显示,这一部分涉及到内容绘制,同样还是基于WinBase类进行窗口开发。创建一个反馈文本的窗口类 WinText,并且准备基本的资源代码如下:// Yez/src/win_text.cj @Abbr // 文本色彩值转Color对象宏 @LifeCycle @HookProc open class WinText <: WinBase{ // 内容纯色画刷 private let _brhCnt:SolidColorBrush // 背景纯色画刷 private let _brhbg:SolidColorBrush // 文本渲染核心 private let _textCore:TextCore // 文本格式 private let _textformat:TextFormat // 文本布局 private let _textLayout:TextLayout // 文本内容 private var _text:String = "" /** 创建窗口 */ public func createWin( x:Int32, y:Int32, nWidth:Int32, nHeight:Int32, hWndParent:RgfHwnd, substrate!:Substrate = Substrate.Direct2D ):RgfHwnd{ ... } /** 文本对象管理方法,根据配置和窗口尺寸生成文本渲染依赖的对象 */ private func createFontObj(){ let rect:Rect = surface.getRect() surface.createTextCore(_textCore) .createTextFormat(_textformat,ConfigMgr.i.fontSize) .createTextLayout(_textLayout,_text,_textformat,Float32(rect.right),Float32(rect.bottom)) } /** 创建设备/渲染相关资源事件 */ public override open func createDeviceResources():Bool{ // 创建 纯黑内容画刷 和 白色半透明背景画刷 surface.createSolidColorBrush(_brhCnt,"#000",1.0) .createSolidColorBrush(_brhbg,"#FFFA",1.0) // 创建文本相关对象 createFontObj() return true; } } 完成渲染事件,确保内容正确渲染// Yez/src/win_text.cj // 绘制窗口 public override open func onPaint(wRect:Rect):Unit{ surface.clear(0.0,0.0,0.0,0.0) // 透明模式 // 渲染背景圆角矩形 surface.fillRoundedRectangle(surface.getRect(),4.0,4.0,_brhbg) // 渲染大模型返回的文本内容 surface.drawTexts(_textCore,_brhCnt,_textLayout,originX:0.0,originY:0.0) } 完成显示文本方法。当设置文本时,若窗口类已经存在,就更新内部文本;若窗口不存在,就根据当前内容创建窗口并显示。// Yez/src/win_text.cj public func showText(str:String,p:RgfHwnd):Unit{ // 获取光标位置 let pos = rsGetCursorPos() // 获取光标所在屏幕矩形范围 let monitor = rsGetMonitorFromPoint(pos) // 如果窗口未创建,就创建窗口 if(!isCreated()){ createWin(pos.x,pos.y,100,100,p) } if(_textLayout.isValid()){ if(str != _text){ // 更新文本内容 _text = str; // 创建文本布局信息 surface.createTextLayout(_textLayout,_text,_textformat,Float32(monitor.right - monitor.left) / 2.0,Float32(monitor.bottom - monitor.top) / 2.0) } // 获取文本自动布局后的测量数据 let m = _textLayout.getMetrics() let w = Int32(m.getOrThrow().width) let h = Int32(m.getOrThrow().height) 按照测量数据移动窗口 if(pos.x > (monitor.right - monitor.left) / 2){ move(pos.x - w,pos.y - h,w,h) }else{ move(pos.x,pos.y - h,w,h) } } // 窗口显示 show(true) } 文本控件的效果如下图所示:注’ 完整的代码请参考提供的代码文件联结各个窗口逻辑完成上述各个控件后,需要实现各个组件之间的联合。实现流式信息与动画的联动// Yez/src/win_anm.cj /** 文本变更事件 */ func onTextChange(str:String):Unit{ if(!_firstText){ _firstText = true // 当文本输出开始,切换“跳跃”动画状态到“说话”动画状态 play(AnmState.Speak) } _text += str _textBox.showText(_text,hWnd) } /** 大模型开始输出文本 */ func onTextStart():Unit{ killTimer(0) _text = "" // 给大模型上发请求后,播放“跳跃”动画 play(AnmState.Jump) _firstText = false } /** 大模型结束输出文本 */ func onTextEnd():Unit{ // 大模型输出结束后,延迟5s,以让用户完整阅读完内容 const cj_bug_val:Option<CFunc<(HWND, UInt32, UInt64, UInt32) -> Unit>> = None setTimer(0,5000,cj_bug_val) } /** 时钟事件 */ func onTimer(e:EvTimer):Unit{ killTimer(0) // 用户阅读完内容后,销毁文本框窗口 _textBox.destroyWin() // 播放“等待”动画 play(AnmState.Wait) } 双击“椰椰”时打开输入框窗口// Yez/src/win_anm.cj // 客户区双击鼠标左键 func onLButtonDblClk(e:EvMouse):Unit{ // 在“等待”状态下双击“椰椰” match(_nowState){ case AnmState.Wait => {=> // 销毁文本框(虽然已无可能还显示,不过保险起见) _textBox.destroyWin() if(!_inputBox.isCreated()){ // 创建文本输入框窗口 _inputBox.createWin(hWnd) }else{ // 如果窗口存在就移动输入框窗口 // 这部分适用于希望输入框跟随鼠标位置的情况 // 对于方案二的桌面居中而言,没有必要 _inputBox.move() } () }() case _ => () } } 结语至此,仓颉AI桌宠“椰椰”就基本开发完成了,其实还有很多规划的内容可以增加,比如当和椰椰说“咔嚓”或者“拍照”时激活截图功能。这些内容就留给读者继续探索啦!本文相关链接本文项目代码库:https://gitcode.com/raozj/YezRGF_CJ库:https://gitcode.com/Cangjie-SIG/RGF_CJRGF_UI库:https://gitcode.com/raozj/RGF_UIRGF案例库:https://gitcode.com/raozj/RGF_CJ_Example华为云ModelArts:https://www.huaweicloud.com/product/modelarts/studio.html
-
我就只最后一个改为0x04
-
一、安装Tomcat下载访问Apache Tomcat官方网站(https://tomcat.apache.org/),根据你的操作系统(如Windows、Linux、macOS)和需求选择合适的版本进行下载。例如,对于开发环境,通常选择较新的稳定版本。安装(以Windows为例)解压下载的ZIP文件到你想要安装的目录,如C:Program FilesApache Tomcat。配置环境变量:将Tomcat的bin目录路径添加到系统的PATH变量中,这样就可以在命令行中方便地运行Tomcat相关命令。例如,在Windows系统中,通过“控制面板”->“系统和安全”->“系统”->“高级系统设置”->“环境变量”来编辑PATH变量。二、启动和停止Tomcat命令行方式(以Windows为例)启动:打开命令提示符,切换到Tomcat安装目录下的bin目录,运行startup.bat文件。这将启动Tomcat服务器,并且可以在控制台看到启动日志,显示服务器初始化的过程,包括加载各种配置文件和组件。停止:运行shutdown.bat文件可以停止Tomcat服务器。在停止过程中,服务器会处理完正在进行的请求,然后优雅地关闭。通过服务方式(以Windows为例)可以将Tomcat安装为Windows服务。在Tomcat的bin目录下,使用service.bat install命令将其安装为服务。之后,可以通过Windows服务管理器(在“运行”中输入services.msc并回车)来启动、停止和管理Tomcat服务。这种方式适合在服务器环境中使用,使得Tomcat可以在系统启动时自动启动。三、部署Web应用程序简单部署(将WAR文件部署到webapps目录)对于一个打包好的Web应用程序(通常是一个.war文件),可以将其直接复制到Tomcat安装目录下的webapps目录中。例如,有一个名为myapp.war的文件,将它放入webapps目录后,Tomcat会自动解压并部署这个应用程序。部署后的应用程序可以通过http://localhost:8080/myapp(假设Tomcat默认端口是8080)来访问,其中myapp是应用程序的上下文路径,它通常和.war文件的名称(去掉.war后缀)相同。通过配置文件部署(server.xml)可以在Tomcat的conf/server.xml文件中配置Web应用程序的部署。找到<Host>标签,在其中添加一个<Context>标签来定义应用程序的部署信息。例如:1<Context path="/myapp" docBase="C:/myapp" reloadable="true"/>这里path属性定义了应用程序的上下文路径(访问该应用程序的URL前缀),docBase属性指定了应用程序的实际物理路径(可以是绝对路径或相对于CATALINA_BASE的路径),reloadable属性设置为true表示当应用程序的类文件或配置文件发生变化时,Tomcat会自动重新加载应用程序。通过Manager App部署Tomcat自带了一个Manager App,可以通过浏览器访问http://localhost:8080/manager/html(默认需要用户名和密码,在conf/tomcat - users.xml文件中配置用户权限)。在Manager App中,可以上传.war文件进行部署,还可以查看已部署应用程序的状态、启动和停止应用程序等操作。四、配置Tomcat端口配置(server.xml)在conf/server.xml文件中,可以修改Tomcat监听的端口。默认情况下,Tomcat使用8080端口用于HTTP请求。找到以下代码段:123<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"/>可以将port属性的值修改为其他端口号,如8081等。如果修改为小于1024的端口(如80),在Linux和macOS系统中可能需要以管理员权限运行Tomcat,因为这些端口是特权端口。虚拟主机配置(server.xml)可以在server.xml文件中配置虚拟主机,使得Tomcat能够根据不同的域名来提供不同的Web服务。例如:12345678910<Host name="www.example1.com" appBase="webapps1" unpackWARs="true" autoDeploy="true"> <Alias>example1.com</Alias> <Context path="" docBase="C:/webapps1/root" /></Host><Host name="www.example2.com" appBase="webapps2" unpackWARs="true" autoDeploy="true"> <Alias>example2.com</Alias> <Context path="" docBase="C:/webapps2/root" /></Host>这里定义了两个虚拟主机,www.example1.com和www.example2.com,它们分别对应不同的应用程序目录(webapps1和webapps2),当用户访问不同的域名时,Tomcat会根据虚拟主机的配置提供相应的服务。日志配置(logging.properties)Tomcat的日志配置文件是conf/logging.properties。可以在其中配置日志的级别、输出位置等。例如,可以修改控制台日志的级别,将以下行:1java.util.logging.ConsoleHandler.level = INFO中的INFO修改为FINE或其他级别(SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST)来调整日志的详细程度。还可以配置将日志输出到文件,通过修改handlers属性和相关的Handler配置部分来实现。五、与开发工具集成Eclipse集成在Eclipse中开发Java Web应用程序时,可以很方便地集成Tomcat。首先确保已经安装了Eclipse的Web开发插件(如Eclipse IDE for Enterprise Java Developers)。配置Tomcat:在Eclipse的“Servers”视图中,右键单击并选择“New”->“Server”,然后选择“Apache Tomcat”版本,点击“Next”。在接下来的步骤中,指定Tomcat的安装目录,完成配置。部署应用程序:在Eclipse中开发好Web应用程序后,可以通过右键单击项目,选择“Run As”->“Run on Server”,将应用程序部署到Tomcat服务器并运行,方便进行调试和开发。IntelliJ IDEA集成在IntelliJ IDEA中,同样需要先安装支持Web开发的插件。配置Tomcat:在“Run/Debug Configurations”中,点击“+”添加一个“Tomcat Server”配置。在配置对话框中,指定Tomcat的安装目录、JDK版本等信息。部署和运行:将Web应用程序添加到配置的Tomcat服务器中,然后可以通过点击“Run”或“Debug”按钮来启动Tomcat并运行应用程序,方便在开发过程中进行调试和测试。
-
Hisec Endpoint 的驱动会导致 Windows 11 的 DirectStorage API 无法正常工作,Xbox Game Bar 的设置-游戏功能界面会提示“由于驱动程序 EdrDriver,BypassIO 不受支持”,影响游戏性能,而卸载 Hisec Endpoint 可以解决此问题(Xbox Game Bar 显示“C:\ 支持 DirectStorage”,表明 DirectStorage API 恢复正常)。望华为相关项目组后续改善该问题。
-
一、软件定位与核心功能GIMP(GNU Image Manipulation Program)是开源跨平台图像编辑工具,支持图层管理、高级修图、色彩校正等功能,被广泛应用于平面设计、照片修复等领域。相比商业软件,其优势包括:全功能免费:支持PSD文件编辑、CMYK色彩模式插件生态丰富:600+扩展插件增强功能硬件要求低:1GB内存即可流畅运行二、安装环境准备1. 系统要求项目最低配置推荐配置操作系统Windows 7Windows 10/11内存1GB4GB+存储空间200MB1GB SSD2. 运行库检测需安装最新版 Microsoft Visual C++ Redistributable(安装包已集成,无需单独下载)三、安装流程详解步骤1:获取安装包访问GIMP官方安装包下载页:https://pan.quark.cn/s/5de67e6f6176/,选择 gimp-2.10.38-setup-1.zip步骤2:启动安装程序解压压缩包, 右键以管理员身份运行 gimp-2.10.38-setup-1.exe 文件,选择简体中文安装语言:步骤3:左下角点击自定义,设置安装路径建议修改默认路径(示例):D:\DesignTools\GIMP 2.10 步骤4:选择组件安装勾选以下核心组件:☑️ 核心程序文件☑️ 附加图标集☑️ Python脚本支持(开发扩展必备)步骤5:完成安装勾选 创建桌面快捷方式,等待进度条完成后点击完成四、首次运行配置1. 初始化设置首次启动时选择工作区布局(推荐 Default 或 Dark 主题)2. 中文语言包安装通过 Edit > Preferences > Interface > Language 选择「简体中文」,重启生效五、功能验证测试1:基础图像处理打开示例图片,使用 自由选区工具 进行抠图操作测试2:插件扩展安装通过 菜单 > 帮助 > 显示插件浏览器 安装第三方插件(如G'MIC图像特效库)六、常见问题解答Q1:启动时报错"MSVCP140.dll 丢失"?安装Visual Studio 2015+运行库或重新运行安装包勾选 Install runtime 选项Q2:如何导出PSD文件?通过 文件 > 导出为 选择Photoshop格式,保留图层信息Q3:软件界面显示异常?重置用户配置:删除 C:\Users\用户名\.gimp-2.10 文件夹七、延伸学习GIMP官方手册:https://docs.gimp.org/2.10/zh_CN/声明:本教程使用GIMP官方安装包制作,遵循GPLv3开源协议。原创内容转载请注明来源。
-
什么是 PYTHONPATH?PYTHONPATH 是一个环境变量,它告诉 Python 解释器在哪些目录中查找要导入的模块。这对于包含不在标准目录中的自定义模块非常有用。Linux 系统中设置 PYTHONPATH 环境变量在 Python 开发环境中,正确设置 PYTHONPATH 环境变量对于确保 Python 解释器能够找到并导入自定义模块至关重要。以下是如何在 Linux 系统中设置或更新 PYTHONPATH 的简洁指南。如何设置 PYTHONPATH?以下是在 Linux 系统中设置 PYTHONPATH 的步骤:打开终端。使用文本编辑器打开您的 shell 配置文件。根据您的 shell 和系统,这可能是 .bashrc、.bash_profile 或 .profile 文件。例如,如果您使用的是 bash,可以编辑 .bashrc 文件:1vi ~/.bashrc在打开的配置文件中,添加以下行来设置或更新 PYTHONPATH:1export PYTHONPATH="${PYTHONPATH}:/path/to/your/module1:/path/to/your/module2"这里的 /path/to/your/module1 和 /path/to/your/module2 应该替换为您想要添加到 PYTHONPATH 中的实际路径。保存并关闭文件。为了让更改立即生效,您需要重新加载配置文件。可以通过以下命令完成:1source ~/.bashrc或者,如果您编辑的是 .bash_profile 或 .profile,则使用:1source ~/.bash_profile或1source ~/.profile示例假设您有两个模块目录 /home/user/my_project/libs 和 /home/user/other_project/libs,您想要将它们添加到 PYTHONPATH 中,您可以这样操作(路径之间用冒号隔开):1export PYTHONPATH="${PYTHONPATH}:/home/user/my_project/libs:/home/user/other_project/libs"这行代码会追加新的路径到现有的 PYTHONPATH 变量中,而不会覆盖原有的设置。小贴士使用 ${PYTHONPATH} 可以确保您不会丢失任何现有的 PYTHONPATH 设置。如果您使用的是其他 shell(如 zsh 或 fish),则配置文件可能是 .zshrc 或 config.fish。更改 PYTHONPATH 后,新启动的终端会话或脚本将使用新的设置。通过以上步骤,您可以轻松地管理 PYTHONPATH,以便 Python 解释器能够正确地找到您的模块。在 Windows 系统中设置 PYTHONPATH 环境变量的步骤与 Linux 系统有所不同。以下是在 Windows 中设置 PYTHONPATH 的指南:Windows 系统中设置 PYTHONPATH操作打开系统属性对话框:您可以通过右键点击“此电脑”或“我的电脑”图标,然后选择“属性”来打开系统属性。或者,您可以在开始菜单中搜索“系统”并点击它。点击“高级系统设置”:在系统属性窗口中,找到并点击“高级”标签页。在“高级”标签页中,点击“环境变量”按钮。编辑系统变量:在环境变量窗口中,您会在下方看到“系统变量”和“用户变量”两个部分。在“系统变量”部分,找到名为 PYTHONPATH 的变量(如果没有,则点击“新建”来创建一个)。如果找到了 PYTHONPATH,双击它进行编辑;如果需要新建,点击“新建”,然后输入 PYTHONPATH 作为变量名。设置或更新变量值:在变量值字段中,输入您的模块路径。如果之前已经有值,确保不要覆盖它们,而是用分号(;)分隔每个路径。例如:C:\path\to\your\module1;C:\path\to\your\module2点击“确定”保存更改:您需要点击每个打开的对话框的“确定”按钮来保存更改。重启资源管理器或重启计算机:为了让环境变量生效,您可能需要重启文件资源管理器(按 Ctrl + Shift + Esc 打开任务管理器,右键点击“文件资源管理器”并选择“重新启动”)或者重启计算机。示例假设您有两个模块目录 C:\Users\YourUsername\myproject\libs 和 C:\Users\YourUsername\otherproject\libs,您想要将它们添加到 PYTHONPATH 中,您可以按照以下步骤操作:打开环境变量窗口。找到或创建 PYTHONPATH 变量。设置变量值为 C:\Users\YourUsername\myproject\libs;C:\Users\YourUsername\otherproject\libs。通过以上步骤,您就可以在 Windows 系统中设置 PYTHONPATH 环境变量了。
-
在Windows 10环境中,WPF应用有时可能会遇到屏幕键盘无法自动弹出的问题,尤其是在添加了外部输入设备(如扫码头或刷卡器)后。这个问题的发生可能是由于焦点管理或系统输入法状态被这些外部设备打乱引起的。下面是一些可能的解决方案:1. 强制显示屏幕键盘可以通过代码强制调用屏幕键盘。在WPF应用中,当需要显示屏幕键盘时,可以使用以下代码:using System.Diagnostics; private void ShowTouchKeyboard() { Process.Start(@"C:\Program Files\Common Files\Microsoft Shared\Ink\TabTip.exe"); }你可以在点击输入框时调用这个方法,以确保屏幕键盘弹出。例如,可以订阅输入框的GotFocus事件:private void TextBox_GotFocus(object sender, RoutedEventArgs e) { ShowTouchKeyboard(); }2. 检查设备影响如果问题是由扫描头或刷卡器引起的,可以尝试以下步骤:设备驱动更新:确保所有外部设备的驱动程序都是最新的。设备配置检查:检查外部设备的配置,看看是否提供了选项来避免干扰系统输入法。3. 使用 Windows 触摸服务确保与触摸和输入相关的服务在Windows 10上正确运行,例如“Touch Keyboard and Handwriting Panel Service”。4. 设置 WPF 应用为触摸优先您可以将WPF应用设置为触摸优先,确保输入焦点在触摸交互时始终正确处理。可以在App.xaml.cs中设置:protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 设置应用为触摸优先 if (Tablet.TabletDevices.Count > 0) { Tablet.TabletDevices.First().CaptureMode = CaptureMode.None; } }5. 重新设置输入焦点当发现屏幕键盘不会自动弹出时,可以尝试在点击输入框时强制设置输入焦点。可以在输入框的GotFocus事件中加入以下代码:private void TextBox_GotFocus(object sender, RoutedEventArgs e) { var textBox = sender as TextBox; if (textBox != null && !textBox.IsKeyboardFocused) { textBox.Focus(); ShowTouchKeyboard(); } }综合示例综合以上方法,你可以这样调整你的WPF应用:using System; using System.Diagnostics; using System.Linq; using System.Windows; namespace YourNamespace { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.Loaded += MainWindow_Loaded; } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { if (Tablet.TabletDevices.Count > 0) { Tablet.TabletDevices.First().CaptureMode = CaptureMode.None; } } private void TextBox_GotFocus(object sender, RoutedEventArgs e) { var textBox = sender as TextBox; if (textBox != null && !textBox.IsKeyboardFocused) { textBox.Focus(); ShowTouchKeyboard(); } } private void ShowTouchKeyboard() { Process.Start(@"C:\Program Files\Common Files\Microsoft Shared\Ink\TabTip.exe"); } } }结论通过上述方法,你应该能够提高WPF应用中触摸输入框的可靠性,确保屏幕键盘在需要时能够正常弹出。如果问题依然存在,可能需要更深入的调试,包括检查外部输入设备与操作系统之间的交互。
-
---2023.7.23 准备材料 srs下载:GitHub - ossrs/srs at 3.0release 目前srs release到5.0版本。 srs官方文档:Introduction | SRS (ossrs.net) Docker下载:Download Docker Desktop | Docker 进入docker官网选择window版本直接下载。由于srs无法直接部署在Window中,因此需要使用docker容器。 搭建步骤 2.1 安装docker Docker 并非是一个通用的容器工具,它依赖于已存在并运行的 Linux 内核环境。 Docker 实质上是在已经运行的 Linux 下制造了一个隔离的文件环境,因此它执行的效率几乎等同于所部署的 Linux 主机。 在安装docker之前先要打开window的虚拟机。 2.2.1 安装 Hyper-V Hyper-V 是微软开发的虚拟机,类似于 VMWare 或 VirtualBox,仅适用于 Windows 10。这是 Docker Desktop for Windows 所使用的虚拟机。 但是,这个虚拟机一旦启用,QEMU、VirtualBox 或 VMWare Workstation 15 及以下版本将无法使用!如果你必须在电脑上使用其他虚拟机(例如开发 Android 应用必须使用的模拟器),请不要使用 Hyper-V! 1、开启 Hyper-V 1)鼠标移动到window图标上,有机鼠标右键。点击选择“应用和功能”。 2)下拉弹出框右边的滑动条到最底下,点击“程序和功能”。 3)点击“启动或者关闭window功能” 4)勾选Hyper-V 至此Hyper-v打开完成,启动可以使用命令也可以点击鼠标右键启动。 启动命令如下: Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All 2.2.2 安装docker 双击下载的 Docker for Windows Installer 安装文件,一路 Next,点击 Finish 完成安装。 在cmd中输入docker出现docker 命令格式则说明安装成功了。 2.2、编译安装srs服务器 1、启动window中的cmd控制台窗口。 2、启动docker并映射srs文件的命令。 docker run -it --name=srs -v /G/srs:/tmp/srs -w /tmp/srs/trunk -p 1935:1935 -p 1985:1985 -p 8080:8080 -p 8085:8085 registry.cn-hangzhou.aliyuncs.com/ossrs/srs:dev bash 这个命令我是链接的G:\srs\路径。 这里不能使用pwd和window的路径格式。例如G:\srs要修改为/G/srs。 执行完成之后,执行ls命令,出现srs的文件结构就说明映射成功了。 3、docker启动映射成功之后执行./configure。 此处可能出现问题: bad interpreter: No such file or directory 解决方法如下: vi configure :set ff :set fileformat=unix :wq 4、修改问题之后继续执行 ./configure。出现如下界面说明./configure执行成功。 5、 执行make命令。出现如下界面说明make成功了。 6、执行 ./objs/srs -c conf/srs.conf命令启动srs服务。出现如下界面说明启动成功了。 至此,srs服务启动成功。 SRS测试使用方法 一、ffmpeg推拉流测试 1、使用ffmpeg命令推流 ffmpeg -re -i C:\Users\admin\Desktop\ship\pasf2.flv -vcodec copy -acodec copy -f flv -y rtmp://192.168.10.103/live/livestream 以上命令中的ip地址是服务器所在的window系统的ip地址。 使用ffmpeg命令拉流 ffplay rtmp:///live/livestream 二、WebRTC推拉流 1、使用ffmpeg命令推流 ffmpeg -re -i C:\Users\admin\Desktop\ship\pasf2.flv -vcodec copy -acodec copy -f flv -y rtmp://192.168.10.103/live/livestream 以上命令中的ip地址是服务器所在的window系统的ip地址。 2、使用WebRTC拉流 推送流成功之后,使⽤srs⾃带的rtc_player播放器进⾏播放,直接请求srs服务的8080端⼝即可。 在浏览器中输入如下网址: http://192.168.10.103:8080/players/srs_player.html 三、VLC推拉流 四、docker命令 1、查看decker中运行的容器 docker ps -a 2、 获取容器/镜像的元数据 docker inspect [OPTIONS] NAME|ID docker inspect registry.cn-hangzhou.aliyuncs.com/ossrs/srs:dev 3、连接到正在运行中的容器 docker attach [OPTIONS] CONTAINER 问题:Error response from daemon: No such container: registry.cn-hangzhou.aliyuncs.com/ossrs/srs:dev 解决办法:出现此问题的原因是由于docker容器没有启动,需要启动docker容器。 链接命令:docker attach 39031b94cb0e 4、启动docker容器 docker start 39031b94cb0e 5、停止docker容器 docker stop 39031b94cb0e 6、直接关闭容器 docker kill 容器ID或容器名 7、重启docker容器 docker restart 容器ID或容器名 五、srs命令 1、指定配置文件,即可启动srs ./objs/srs -c conf/srs.conf 2、启动srs ./etc/init.d/srs start 3、查看SRS状态 ./etc/init.d/srs status 4、停止SRS ./etc/init.d/srs stop 5、重启SRS ./etc/init.d/srs restart 6、Reload SRS ./etc/init.d/srs reload ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/qq_43812868/article/details/134141219
-
二月问题总结如下:【1】在ECS windows部署Llama2 尝试使用MLC运行,但出现以下报错,求助cid:link_0【2】atlas300P3 在容器中访问rtsp流地址报错No route to host cid:link_1【3】ECS上面,我看机器学习推荐的只有N卡,想问下华为自己的显卡在ModelArts那边不是能用,为啥还没上ECS cid:link_2【4】昇腾310(Ascend 310)能不能用来搭建stable diffusecid:link_3【5】 acl init failed, errorCode = 100039cid:link_4
-
1 什么是OpenRestyOpenResty是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。简单地说OpenResty 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。2 OpenResty安装·下载地址 http://openresty.org/cn/download.html选择符合自己电脑的安装包下载完成后,选择安装目录(建议非中文)进行解压双击nginx.exe运行即可测试:打开浏览器访问 localhost:80
-
在不久的将来,鸿蒙PC系统能否成为Windows的真正竞争者?
-
Scoop是一个强大的Windows软件包管理器,在Windows用户中相当受欢迎。它旨在简化和加速在Windows操作系统上的软件安装和管理。通过命令行操作,用户可以轻松地搜索、安装、更新和卸载各种应用程序,而无需手动浏览官方网站或执行繁琐的安装步骤。Scoop的重要性在于为用户提供了一种便捷的方式,使其能够快速、一致地配置其开发环境或安装常用工具,提高了Windows系统上软件管理的效率和灵活性。建议增加 Scoop 镜像。
-
可以按照以下步骤来检查自己的操作系统是否支持 64 位架构: 1:打开"此电脑"或"我的电脑"等文件资源管理器; 2:右键单击计算机图标或界面空白处,选择属性选项; 3:在弹出的窗口中,可以看到基本信息部分,其中有一个 “系统类型” 的栏目,如果显示 "64 位操作系统",则表示你的操作系统支持 64 位架构;如果显示 "32 位操作系统",则表示你的操作系统只支持 32 位架构。
-
1、背景23.3版本管理中心新特性增加了支持连接器参数配置,当任务使用无人值守执行器运行时,支持对无人值守执行器的连接器参数进行配置:RDP窗口全屏显示RDP桌面缩放比RDP附加参数2、参数说明RDP附件参数官方列举的见右边链接:https://github.com/awakecoding/FreeRDP-Manuals/blob/master/User/FreeRDP-User-Manual.markdown如果要导出完整的参数信息,按以下步骤操作:①进入无人值守连接器rdp目录连接器安装目录\agent\freerdp (连机器版本是3.0.0及以上版本为该目录)连接器安装目录\agent\wfreerdp (连接器版本是3.0.0以下版本为该目录)②在以上目录输入cmd打开命令行窗口,然后输入命令:wfreerdp.exe /help > help.txt 就可以把包含完整参数信息的help.txt导出到rdp目录下。注意事项:1. 关闭RDP窗口全屏显示后可以设置窗口分辨率的宽、高2. 23.3管理中心配置的RDP连接参数只有在3.2.0及以上版本的连接器上才能生效
-
前言 Windows操作系统一般是安装在磁盘驱动器的C盘中,运行时会产生许多垃圾文件,C盘空间在一定程度上会越来越小。 伴随着电脑工作的时间越久,C盘常常会提示显示其内存已不足。 C盘容量不足将会极大影响系统的运行速度,电脑会变卡、死机。 释放磁盘空间可以提高计算机的性能。那么,如何正确清理C盘呢? 清理方法 1. 利用Windows自己附带的磁盘清理工具 1.鼠标右击【C盘】选择【属性】。 2.右击【磁盘清理】。 3.勾选需要清理的文件,点击【清理系统文件】,清理完成后,点击【确定】即可。 2. 开启自动清理 开启自动清理后,Windows可以通过删除不需要的文件自动释放空间。 进入【设置】—【系统】—【存储】—将【存储感知】设置为【开】,电脑就会自动清理。 3. 通过“配置存储感知或立即运行”来清理 进入【设置】—【系统】—【存储】—【配置存储感知或立即运行】 点击【立即清理】 4. 管理C盘中的程序 进入【设置】—【系统】—【存储】—【应用和功能】 进入以后,我们把【筛选条件】设置成【window10 C:】 3. 通过卸载C盘下的一些不必要软件,可以进一步释放C盘空间。 如果遇到自己不认识的软件可以百度:xxx可以卸载吗?这样就可以避免误卸载掉一些有用的软件。 5. 系统文件夹转移 . 进入【设置】—【系统】—【存储】—【更多存储设置】—【更改新内容保存位置】—【选择别的盘符】— 点击【应用】 6. 将C盘现有内容转移到别的盘 手动转移:打开【此电脑】我们可以看到【文档】、【下载】、【桌面】等文件夹。它们目前默认的路径都是C盘。 右键【桌面】— 选择【属性】 【位置】— 【移动】— 【选择一个别的盘的路径】 最好提前在其他盘中建一个同名的文件夹,如Desktop,移进去就好。 ———————————————— 版权声明:本文为CSDN博主「想变厉害的大白菜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_44211968/article/details/120129476
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签