食用方法 Step 1 将ChineseSimplified.isl放到Inno Setup安装目录下的"Languages"文件夹里面 Step 2 如果你是通过新建脚本的方式创建脚本,在Languages选项勾选Chinese Simplified即可: 如果你需要在现有脚本中添加简体中文支持 直接在你的脚本的[Languages]部分添加下面一行即可 1 Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" 示例: 1 2 3 [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" 注意:此翻译版本支持 Inno Setup 6.1.0+ 的软件,Inno Setup 5 的翻译文件在这里 查看6.1.0+和6.0.0+的区别 查看6.0.3+和6.0.0+的区别 链接 Inno Setup issrc
计算切片间距的方法: 对于CT扫描出的断层图像,没有存储Spacing Between Slices信息,但可以利用位置信息计算得到。 需要先读取相邻两层切片的位置信息,假设为pos1和pos2,然后计算两个位置的距离即为切片间距。 1 2 3 4 5 // double pos1[3], pos2[3]; double spacing = sqrt( (pos1[0] - pos2[0]) * (pos1[0] - pos2[0]) + (pos1[1] - pos2[1]) * (pos1[1] - pos2[1]) + (pos1[2] - pos2[2]) * (pos1[2] - pos2[2]));
GDI+绘制椭圆时只支持输入一个矩形范围,无法绘制倾斜的椭圆。 绘制椭圆的 API: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // 摘要: 绘制边界 System.Drawing.RectangleF 定义的椭圆。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // rect: System.Drawing.RectangleF 结构,它定义椭圆的边界。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, RectangleF rect); // // 摘要: 绘制一个由边框(该边框由一对坐标、高度和宽度指定)定义的椭圆。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // x: 定义椭圆的边框的左上角的 X 坐标。 // y: 定义椭圆的边框的左上角的 Y 坐标。 // width: 定义椭圆的边框的宽度。 // height: 定义椭圆的边框的高度。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, float x, float y, float width, float height); // // 摘要: 绘制边界 System.Drawing.Rectangle 结构指定的椭圆。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // rect: System.Drawing.Rectangle 结构,它定义椭圆的边界。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, Rectangle rect); // // 摘要: 绘制一个由边框定义的椭圆,该边框由矩形的左上角坐标、高度和宽度指定。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // x: 定义椭圆的边框的左上角的 X 坐标。 // y: 定义椭圆的边框的左上角的 Y 坐标。 // width: 定义椭圆的边框的宽度。 // height: 定义椭圆的边框的高度。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, int x, int y, int width, int height); 可以看到,绘制椭圆的API,都只支持输入一个矩形区域,长和宽都只能是水平或者竖直的,因此,绘制出椭圆的长轴和短轴也是水平或竖直的,而无法绘制一个旋转过的椭圆,如下图: 给定椭圆的4个顶点,绘制出椭圆。虽然不能直接调用API绘制椭圆,但是可以通过绘制4条连续的贝塞尔曲线来闭合成一个椭圆。 1 2 3 4 5 6 7 8 9 10 // // 摘要: 用 System.Drawing.PointF 结构数组绘制一系列贝塞尔样条。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // points: System.Drawing.PointF 结构的数组,这些结构表示确定曲线的点。 // 此数组中的点数应为 3 的倍数加 1,如 4、7 或 10。 // // 异常: // T:System.ArgumentNullException: pen 为 null。- 或 -points 为 null。 public void DrawBeziers(Pen pen, PointF[] points); 使用这种方法实际上只需要知道椭圆的中心点,两个轴的长度以及旋转角度即可,而这些都可以通过4个顶点计算得到。 根据以上的信息,依次计算出连续贝塞尔曲线的13个控制点,然后调用DrawBeziers绘制椭圆。 各个点的位置如图: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 /** * C# */ // MAGICAL CONSTANT to map ellipse to beziers // 2/3*(sqrt(2)-1) const double Ellipse2Beziers = 0.2761423749154; // GDI Bitmap using var bitmap = new System.Drawing.Bitmap(width, height); using var graphics = System.Drawing.Graphics.FromImage(bitmap); // 椭圆的4个顶点 PointF[] ellipse = new PointF[4] { point1, point2, point3, point4 }; // 两个轴的长度 double r1 = ellipse[2].DistanceTo(ellipse[3]); double r2 = ellipse[0].DistanceTo(ellipse[1]); // 旋转角度 double angle = -SysMath.Atan2(ellipse[2].Y - ellipse[3].Y, ellipse[2].X - ellipse[3].X); double sin = SysMath.Sin(angle); double cos = SysMath.Cos(angle); // 贝塞尔曲线控制点相对于中心点的偏移长度 SizeF offset = new SizeF((float)(r1 * Ellipse2Beziers), (float)(r2 * Ellipse2Beziers)); // 椭圆中心点 PointF center = new PointF((ellipse[0].X + ellipse[1].X) / 2f, (ellipse[0].Y + ellipse[1].Y) / 2f); // 贝塞尔曲线的控制点 PointF[] beziers = new PointF[13] { new PointF((float)(center.X - r1 / 2.0), center.Y), new PointF((float)(center.X - r1 / 2.0), center.Y - offset.Height), new PointF(center.X - offset.Width, (float)(center.Y - r2 / 2.0)), new PointF(center.X, (float)(center.Y - r2 / 2.0)), new PointF(center.X + offset.Width, (float)(center.Y - r2 / 2.0)), new PointF((float)(center.X + r1 / 2.0), center.Y - offset.Height), new PointF((float)(center.X + r1 / 2.0), center.Y), new PointF((float)(center.X + r1 / 2.0), center.Y + offset.Height), new PointF(center.X + offset.Width, (float)(center.Y + r2 / 2.0)), new PointF(center.X, (float)(center.Y + r2 / 2.0)), new PointF(center.X - offset.Width, (float)(center.Y + r2 / 2.0)), new PointF((float)(center.X - r1 / 2.0), center.Y + offset.Height), new PointF((float)(center.X - r1 / 2.0), center.Y) }; // 旋转变换 double offsetX = center.X - center.X * cos - center.Y * sin; double offsetY = center.Y + center.X * sin - center.Y * cos; for (int j = 0; j < beziers.Length; j++) { beziers[j] = new PointF( (float)(beziers[j].X * cos + beziers[j].Y * sin + offsetX), (float)(beziers[j].Y * cos - beziers[j].X * sin + offsetY)); } // 绘制曲线 graphics.DrawBeziers(new Pen(Brushes.White, 1f)/* Pen */, beziers); 通过上面代码就可以完美的绘制出一个旋转任意角度的椭圆了。 参考 MFC上如何绘制一个可以旋转的椭圆 Drawing Rotated and Skewed Ellipses
规则 私有 Tag (gggg, xxxx) Group number (gggg) 必须为奇数 (odd),并且 (0001, xxxx),(0003, xxxx),(0005, xxxx),(0007, xxxx),(FFFF, xxxx) 不允许使用。 (gggg, 0000) were Group Length Elements, which have been retired.(已弃用) (gggg, 0001-000F),(gggg, 0100-0FFF) 不允许使用。 (gggg, 0010-00FF) 供私有tag创建者(Private Creator)使用,用于在该group中插入一个未使用的标识码(identification code),私有标识码的VR应该为LO (Long String),VM应该为1。 (gggg, 1000-FFFF) 为 Data Element。 Private Creator 和 Data Element 的对应关系为: 例: Data Element (0029, 1000-10FF) 的 Private Creator 是 (0029, 0010) Data Element (0029, 1100-11FF) 的 Private Creator 是 (0029, 0011) Data Element (0029, 1200-12FF) 的 Private Creator 是 (0029, 0012) …… Data Element (0029, FF00-FFFF) 的 Private Creator 是 (0029, 00FF) 标准 http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.8.html
首先需要安装Docker 安装完成后,拉取以下3个仓库 1 2 3 $ docker pull dcm4che/slapd-dcm4chee:2.4.56-23.1 $ docker pull dcm4che/postgres-dcm4chee:13.1-23 $ docker pull dcm4che/dcm4chee-arc-psql:5.23.1 拉取完成后,就可以启动服务了,这里有两种方式,一种是使用Docker命令行依次启动上面3个服务,不过比较麻烦,也容易出错,另一种是直接使用Docker Copmose,相对来说要简单很多。这里就直接使用 Docker Compose的方式。 创建以下两个文件: docker-compose.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 version: "3" services: ldap: image: dcm4che/slapd-dcm4chee:2.4.56-23.1 logging: driver: json-file options: max-size: "10m" ports: - "389:389" env_file: docker-compose.env volumes: - /var/local/dcm4chee-arc/ldap:/var/lib/openldap/openldap-data - /var/local/dcm4chee-arc/slapd.d:/etc/openldap/slapd.d db: image: dcm4che/postgres-dcm4chee:13.1-23 logging: driver: json-file options: max-size: "10m" ports: - "5432:5432" env_file: docker-compose.env volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /var/local/dcm4chee-arc/db:/var/lib/postgresql/data arc: image: dcm4che/dcm4chee-arc-psql:5.23.1 logging: driver: json-file options: max-size: "10m" ports: - "8080:8080" - "8443:8443" - "9990:9990" - "9993:9993" - "11112:11112" - "2762:2762" - "2575:2575" - "12575:12575" env_file: docker-compose.env environment: WILDFLY_CHOWN: /opt/wildfly/standalone /storage WILDFLY_WAIT_FOR: ldap:389 db:5432 depends_on: - ldap - db volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /var/local/dcm4chee-arc/wildfly:/opt/wildfly/standalone - /var/local/dcm4chee-arc/storage:/storage docker-compose.env 1 2 3 4 5 TZ=Asia/Shanghai STORAGE_DIR=/storage/fs1 POSTGRES_DB=pacsdb POSTGRES_USER=pacs POSTGRES_PASSWORD=pacs 其中TZ是用来设置时区的。 启动 1 $ docker-compose -p dcm4chee up -d 停止 1 $ docker-compose -p dcm4chee stop 重新启动 1 $ docker-compose -p dcm4chee start 删除 1 $ docker-compose -p dcm4chee down 参考 Run minimum set of archive services on a single host
前言 The Medical Imaging Interaction Toolkit (MITK)是一个免费的开源软件,用于开发交互式医学影像处理软件。最近突然安排我做相关的一些工作,首先就要从编译开始,当然官网也有编译好的版本,可以直接下载使用。本来在Windows上编译这种开源的软件就很麻烦,在加上github上的东西下载巨慢,常常出错,折腾了好久才编译完成,这里就记录一下踩过的那些坑。 准备工作 Visual Studio 2017 CMake (>=3.19) Qt 5.12.10 (>=5.12.9) Python3 Git OpenSSL 安装包 Doxygen MITK MITK-Diffusion 我这里使用的是Visual Studio 2017版本,2019应该也可以。 Qt需要安装5.12.9以上的版本,官方编译似乎用的5.12.10,所以我这里也选择5.12.10版本,安装过程中尽量把所有组件都选上,因为编译时会使用到很多组件。 CMake,Python,OpenSSL都选择64位版本安装。 最后就是用git克隆下MITK和MITK-Diffusion的仓库。 由于MITK在编译的过程中会下载一些第三方的软件包,所以要能克隆github上的仓库才行,最好有梯子,我就是这里卡了很久。 CMake 相关配置 在MITK仓库目录下新建build文件夹(名字可以随意),然后打开cmake GUI工具,填写source code和build文件夹路径 点击Configure按钮,第一次会弹出对话框选择编译器,根据需要配置即可,这里选择msvc-2017,x64 此时会出现错误提示,找不到Qt,需要手动设置一下Qt5的路径,找到Qt5_DIR项,在Value中填写路径,例:D:/Qt/Qt5.12.10/msvc2017_64/lib/cmake/Qt5,再次点击Configure按钮 检查是否所有的安装路径都正确被检测到(Qt,OpenSSL),确保没有选项是红色 如果只需要编译MITK,那么就可以直接跳到第9步 找到MITK_EXTENSION_DIRS选项,在Value中填写MITK-Diffusion仓库的路径,再次点击Configure按钮 找到MITK_BUILD_CONFIGURATION选项,Value设置为DiffusionRelease,再次点击Configure按钮 此时下方输出会报错,提示找不到NumPy,需要先安装python库NumPy,打开终端,执行pip3 install --user NumPy,完成后再次点击Configure按钮,确保没有错误,没有选项是红色 点击Generate按钮,下方输出Generating done之后,点击Open Project按钮 附上我的配置: 编译 直接编译ALL_BUILD项目即可,但此时编译可能会有一堆莫名奇妙的错误,可以先到MITK仓库目录下找到CMakeExternals目录,然后将里面所有的文件换行符改为Windows下的换行符(CRLF),可以使用VS Code或者Notepad++等工具,过程会有点枯燥。 确保网络可用,最好有国外朋友帮忙,因为编译过程中会下载很多github上的仓库,没有国外朋友帮忙很容易出错。编译ALL_BUILD项目,第一次编译会非常慢,通常需要几个小时,建议先去忙点其它事情。 编译过程中经常会遇到警告被视为错误,没有生成object,导致编译出错,双击错误,打开错误文件,然后再找到错误文件的位置,用记事本打开,选择文件→另存为,选择保存编码为Unicode,再次编译。 在编译MITK-Diffusion的过程中,可能会遇到一些类型转换的报错,如无法将itk::Point转换为mitk::PointSet::PointType等,可能一些编译器能通过,但msvc会报错,只需要稍微修改一下,将出错的参数强制转换成对应类型即可,如:mitk::PointSet::PointType(itkPoint)。 重复3、4若干次,直到编译成功。 编译后的可执行文件在build/MITK-build/bin目录下。
常用图像像素相关的一些Tag Tag VR Keyword (0028,0002) US Samples Per Pixel (0028,0004) CS Photometric Interpretation (0028,0006) US Planar Configuration (0028,0010) US Rows (0028,0011) US Columns (0028,0100) US Bits Allocated (0028,0101) US Bits Stored (0028,0102) US High Bit (0028,0103) US Pixel Representation (7FE0,0010) OB/OW Pixel Data 相关Tag说明 Sample Per Pixel Samples per Pixel (0028,0002) is the number of separate planes in this image. One and three image planes are defined. Other numbers of image planes are allowed, but their meaning is not defined by this Standard. For monochrome (gray scale) and palette color images, the number of planes is 1. For RGB and other three vector color models, the value of this Attribute is 3. Samples Per Pixel 指此图像中平面的个数。对于灰度图像,它的值为1,对于RGB等彩色图像,它的值为3。 听起来可能比较拗口,简单解释一下,对于灰度的图像,它只有一个灰度值,所以是1,而彩色的图像通常是由RGB三个通道混合而成,它的值为3。 Photometric Interpretation 指定解析图像像素的格式。个人比较习惯叫它图像类型,可以根据这个tag判断图像是灰度还是彩色图像。 MONOCHROME1 灰度图,最小值显示为白色,像素值越大就越黑。 MONOCHROME2 灰度图,最小值显示为黑色,像素值越大就越亮。这应该是最常用的格式了。 PALETTE COLOR 自带调色板的图像,显示出来是彩色的,所以属于彩图。当使用它时,Samples Per Pixel的值必须为1。并且必须要有RGB三种颜色的调色板查找表,它的像素值(Pixel Data)用于查找表。 The pixel value is used as an index into each of the Red, Blue, and Green Palette Color Lookup Tables (0028,1101-1103&1201-1203). RGB 彩色图像,每个像素由RGB三种颜色组成,Samples Per Pixel值必须为3。 YBR_FULL 通过色度信号来表示颜色的格式,每个像素由一个亮度Y(luminance)和两个色度Cb(蓝色)、Cr(红色)组成。Samples Per Pixel值为3。 $$Y=+0.2990R+0.5870G+0.1140B$$ $$Cb=-0.1687R-0.3313G+0.5000B+128$$ $$Cr=+0.5000R-0.4187G-0.0813B+128$$ YBR_FULL_422 类似于YBR_FULL,通过色度信号来表示颜色的格式,每个像素点都有对应的亮度Y(luminance),每两个像素点采集一次色度信号,缺少的色度信息通过内插补点的方式运算得到。 Samples Per Pixel的值应该为3,Planar Configuration的值必须是0,像素存储的格式为:Y, Y, Cb, Cr, ... YBR_PARTIAL_422(Retired) 类似于YBR_FULL_422,通过色度信号来表示颜色的格式,不过亮度和色度的计算方式和YBR_FULL的计算方式不同。 $$Y=+0.2568R+0.5041G+0.0979B+16$$ $$Cb=-0.1482R-0.2910G+0.4392B+128$$ $$Cr=+0.4392R-0.3678G-0.0714B+128$$ YBR_PARTIAL_420 类似于YBR_PARTIAL_422,通过色度信号来表示颜色的格式,不同的是,用4:2:2的采样方式时,行方向的色度信息会被丢掉一半,而4:2:0的采样方式,不仅会把行方向的色度信息丢掉一半,列方向的色度信息也会被丢掉一半。色度的采样(Cb,Cr)只有亮度Y(luminance)的 1/4。 Samples Per Pixel的值应该为3,Planar Configuration的值必须是0。 YBR_ICT Irreversible Color Transformation.(不可逆颜色变换) YCbCr的计算方式和YBR_FULL一样。Y为0时表示黑色,Cb,Cr都为0时表示没有颜色。 JPEG 2000有损压缩的彩色图像。Samples Per Pixel的值应该为3,Planar Configuration的值必须是0。 YBR_RCT Reversible Color Transformation.(可逆颜色变换) JPEG 2000无损压缩的彩色图像。Samples Per Pixel的值应该为3。 从RGB转换到YBR_RCT $$Y=floor(\frac{R+2G+B}{4})$$ $$Cb=B-G$$ $$Cr=R-G$$ 从YBR_RCT转换到RGB $$R=Cr+G$$ $$G=Y-floor(\frac{Cb+Cr}{4})$$ $$B=Cb+G$$ 不再使用的格式 HSV、ARGB、CMYK Planar Configuration 指定颜色是按照像素来排列的或是按平面(plane)来排列的。当Samples Per Pixel大于1时应设定此值。 当值为0时表示颜色按像素排列。对于RGB图像,像素的格式为:R1,G1,B1,R2,G2,B2,... 当值为1时表示颜色按平面排列。对于RGB图像,像素的格式为:R1,R2,R3,...Rn,G1,G2,G3,...Gn,B1,B2,B3,...Bn Rows 图像的行数量,即图像的高(Height)。 Columns 图像的列数量,即图像的宽(Width)。 Bits Allocated, Bits Stored, High Bit, Pixel Representation Bits Allocated指定每个像素分配多少位(bit)。值应当为1或者8的倍数。而对于图像像素,Bits Allocated的值通常为8或者16,实际上可以理解为每个像素分配多少字节,因为是8的倍数。 Bits Stored指定存储每个像素占用了多少位(bit),值不能大于Bits Allocated。 High Bit则指定了像素的最高位,通常应该是Bits Stored - 1。 Pixel Representation指定了像素数据的类型。值只能为0或者1,对于彩色图像,值只能为0。 值为0时,表示像素为无符号整型(unsigned integer)。 值为1时,表示像素为2的补码,其实就是有符号整型,即允许存在负数。 这里一定要注意,如果不能正确处理负数,全部按照无符号整型来计算的话,就会遇到符号位的问题,即一个负数会变成一个很大的正数,图像上原本是黑色的区域会变得很亮。 Pixel Data 图像像素。一堆二进制数字,通常会存放在Dicom文件最后的位置。 最后 其实,在写这篇文章之前,一些东西我都还是一知半解的,在写的过程中我也是不断的在查阅资料和源码。其中一些东西难免会掺杂了自己的理解,如果有错误的地方欢迎指正。 参考 DICOM Standard Browser 颜色空间
前言 本文主要记录了EPICS Qt在Linux上的安装步骤。这里以loongnix操作系统为例,Ubuntu系统上编译安装步骤类似。 EPICS Qt是一个基于Qt的分层框架,使用Channel Access (CA) and PV Access(PVA)访问EPICS数据。它是为快速开发控制系统图形界面而设计的,最初是在澳大利亚同步加速器开发的。 安装EPICS 这里不再写具体步骤了,总之就是非常简单,下载、解压、编译即可。具体步骤可以参考以前的文章。 安装Qt 直接使用终端安装Qt 1 2 3 4 5 sudo apt update sudo apt install qtbase5-dev qt5-qmake qtcreator sudo apt install qtdeclarative5-dev qttools5-dev # 安装Qt Svg库,编译QWT时需要用到 sudo apt install libqt5svg5-dev 安装QWT Qt EPICS推荐使用Qwt 6.1.4,如果在Ubuntu 20.04上直接通过终端安装也是这个版本。我使用Qwt 6.2.0编译,也是没有问题的,这里以Qwt 6.2.0为例。 先下载Qwt的源码 下载Qwt-6.2.0。 下载完成后解压 1 2 3 4 # 解压tar.bz2 tar -jxvf qwt-6.2.0.tar.bz2 # 解压zip unzip qwt-6.2.0.zip 解压完成后编译Qwt,使用QtCreator或者在终端使用qmake都可以。 然后手动将编译生成的文件复制到以下位置,例: 1 2 3 4 5 6 7 # 复制编译生成的qwt sudo cp -r build-qwt-unknown-Release/lib/* /usr/lib/loongarch64-linux-gnu/ # 复制编译生成的designer插件 sudo cp build-qwt-unknown-Release/designer/plugins/designer/libqwt_designer_plugin.so /usr/lib/loongarch64-linux-gnu/qt5/plugins/designer/ # 复制qwt头文件 sudo mkdir /usr/include/qwt sudo cp qwt-6.2.0/src/*.h /usr/include/qwt 安装ACAI ACAI Channel Access Interface EPICS Qt依赖ACAI提供的Channel Access接口。 1 2 3 4 5 6 7 8 cd /usr/local/epics/modules/ git clone https://github.com/andrewstarritt/acai.git cd acai vi configure/RELEASE.local # 修改EPICS_BASE路径,例: # EPICS_BASE=/usr/local/epics/base-7.0.7 make -j8 # 等待编译完成 安装google protobuf 如果需要EPICS Qt支持EPICS Archiver Appliance,需要安装google protobuf。 1 sudo apt install protobuf-compiler libprotobuf-dev EPICS Qt 首先克隆EPICS Qt的两个代码仓库。 1 2 3 4 # framework and support libraries git clone https://github.com/qtepics/qeframework.git # QEGui display manager git clone https://github.com/qtepics/qegui.git 这里我将代码都放在~/QtEpics目录。 在开始编译前,需要先配置一些环境变量(根据自己的实际情况设置)。具体可以参考 EPICS Qt Environment Variables 1 2 3 4 5 6 7 8 9 10 11 12 export EPICS_HOST_ARCH=linux-loongarch64 export EPICS_BASE=/usr/local/epics/base-7.0.7 export ACAI=/usr/local/epics/modules/acai export QWT_INCLUDE_PATH=/usr/include/qwt export QWT_ROOT=/usr/lib/loongarch64-linux-gnu export QE_FRAMEWORK="$HOME/QtEpics/qeframework" # 支持PV Access export QE_PVACCESS_SUPPORT=YES # 支持Archiver Appliance export QE_ARCHAPPL_SUPPORT=YES export PROTOBUF_INCLUDE_PATH=/usr/include/google/protobuf export PROTOBUF_LIB_DIR=/usr/lib/loongarch64-linux-gnu 如果环境变量设置了支持Archiver Appliance,需要先编译archapplDataSup。 1 2 cd ~/QtEpics/qeframework/archapplDataSup/ make 编译完成后,可以看到~/QtEpics/qeframework/lib/linux-loongarch64目录下有libarchapplData.a、libarchapplData.so两个文件。 然后依次编译 qeframework qeplugin qegui。EPICS Qt文档说明需要修改configure/RELEASE文件,但我这里修改后似乎没有生效,可能是使用了Qt Creator的原因,只能通过上面的环境变量设置。 注意:这里设置完环境变量,需要直接通过终端打开Qt Creator。 编译qeframework $HOME/QtEpics/qeframework/qeframeworkSup/project/framework.pro 编译qeplugin $HOME/QtEpics/qeframework/qepluginApp/project/qeplugin.pro 编译qegui $HOME/QtEpics/qegui/qeguiApp/project/QEGuiApp.pro 编译过程中可能会遇到一些问题,汇总如下: 找不到Qwt的头文件 解决办法: 修改qeframework/qeframeworkSup/project/common/common.pri 1 2 INCLUDEPATH += $$PWD +INCLUDEPATH += $$(QWT_INCLUDE_PATH) 找不到QEFramework的头文件 解决办法: 修改对应项目的项目文件 1 +INCLUDEPATH += $$(QE_FRAMEWORK)/include 最后将编译生成的文件复制到以下位置,例: 1 2 3 sudo cp ~/QtEpics/qeframework/lib/linux-loongarch64/libarchapplData.so /usr/lib/loongarch64-linux-gnu/ sudo cp ~/QtEpics/qeframework/lib/linux-loongarch64/libQEFramework.so /usr/lib/loongarch64-linux-gnu/ sudo cp ~/QtEpics/qeframework/lib/linux-loongarch64/designer/libQEPlugin.so /usr/lib/loongarch64-linux-gnu/qt5/plugins/designer/ 运行QEGuiApp 1 2 cd ~/epics/qtepics/qegui/bin/linux-loongarch64 ./qegui 运行测试 运行时环境变量设置,例: 1 2 3 export QE_ARCHIVE_TYPE=ARCHAPPL export QE_ARCHIVE_LIST="http://192.168.1.2:17665/mgmt/bpl" export EPICS_CA_ADDR_LIST="192.168.1.2:5732 192.168.1.3:6666" 参考链接 EPICS Qt at GitHub EPICS Qt Getting Started Archiver Appliance Support for EPICS Qt
简介 gRPC是Google开源的一个现代化、高性能的RPC框架,基于HTTP/2标准设计,同时提供多个语言版本,并支持跨语言调用,可以在任何环境中运行。 创建项目 新建解决方案,包含3个项目 gRpcSample 类库,gRPC生成的接口,Server接口、Client接口等 server 控制台程序,服务端 client 控制台程序,客户端 分别给3个项目安装Nuget程序包Grpc并安装所需依赖,然后为gRpcSample项目安装Grpc.Tools和Google.ProtoBuf程序包。 同时,使项目server和client引用项目gRpcSample。 定义服务接口 在gRpcSample项目的文件夹下新建Sample.proto文件,以文本方式打开,修改其中接口定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 syntax = "proto3"; option csharp_namespace = "kira.Interface"; package sampleservice; service SampleService { rpc ServerVersion(VersionRequest) returns (VersionResponse) {} rpc SayHello(HelloRequest) returns (stream HelloResponse) {} } message VersionRequest {} message VersionResponse { string name = 1; string version = 2; } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } 这里指定了生成的类的命名空间,同时定义了两个服务函数,似乎每个函数都必须有参数(请求)和返回(响应),这一点不太清楚。具体的语法有条件的可以参考proto3 language guide。 生成服务接口 在进行下面操作前,建议先将Grpc.Tools拷贝到解决方案目录下,不然的话下面的命令会很长很长… 具体操作是将解决方案下packages\Grpc.Tools.2.23.0-pre1\tools\windows_x64\里面的protoc.exe和grpc_csharp_plugin.exe拷贝到解决方案目录下,完成后就可以进行下一步。 在解决方案目录下打开命令窗口,并执行下面命令 Tip: 直接进入到相应文件夹下,按住Shift键,在空白出单击鼠标右键,就可以看到菜单中多出了一项在此处打开命令窗口(W) 1 $ protoc -IgRpcSample --csharp_out gRpcSample gRpcSample\Sample.proto --grpc_out gRpcSample --plugin=protoc-gen-grpc=grpc_csharp_plugin.exe 执行完没有错误的话,就可以看到gRpcSample项目下多出了两个文件Sample.cs和SampleGrpc.cs,将这两个文件添加到项目gRpcSample。 编译gRpcSample通过。 进行到这里,基本的工作就都完成了,剩下的就是编写服务端和客户端程序了。 服务端程序 service项目新建类MySampleService,并继承SampleService.SampleServiceBase,重写刚刚定义的两个服务接口函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 namespace server { using System.Threading.Tasks; using Grpc.Core; using kira.Interface; public class MySampleService : SampleService.SampleServiceBase { public override Task<VersionResponse> ServerVersion(VersionRequest request, ServerCallContext context) { return Task.FromResult<VersionResponse>( new VersionResponse() { Name = "My Sample Service", Version = "0.0.1.19" }); } public override async Task SayHello(HelloRequest request, IServerStreamWriter<HelloResponse> responseStream, ServerCallContext context) { string[] hellos = { "你好", "Hello", "Hola", "Bonjour", "こんにちは", "hallo" }; foreach (string item in hellos) { await responseStream.WriteAsync(new HelloResponse() { Message = $"{item} {request.Name}" }); } } } } 编写服务启动程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 namespace server { using Grpc.Core; using kira.Interface; class Program { static void Main(string[] args) { Server myServer = new Server() { Services = { SampleService.BindService(new MySampleService()) }, Ports = { new ServerPort("localhost", 10240, ServerCredentials.Insecure) } }; myServer.Start(); System.Console.WriteLine("Sample Server listening on localhost:10240 \nPress any key exit..."); System.Console.ReadKey(); myServer.ShutdownAsync().Wait(); } } } 客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 namespace client { using System.Threading.Tasks; using Grpc.Core; using kira.Interface; class Program { static void Main(string[] args) { Program program = new Program(); program.TestService(); System.Console.ReadKey(); } async void TestService() { Channel channel = new Channel("localhost:10240", ChannelCredentials.Insecure); SampleService.SampleServiceClient cli = new SampleService.SampleServiceClient(channel); VersionResponse ver = cli.ServerVersion(new VersionRequest()); System.Console.WriteLine("Remote Service Version: {0} - v{1}", ver.Name, ver.Version); AsyncServerStreamingCall<HelloResponse> greetings = cli.SayHello(new HelloRequest() { Name = "gRPC" }); IAsyncStreamReader<HelloResponse> stream = greetings.ResponseStream; while (await stream.MoveNext()) { System.Console.WriteLine(stream.Current.Message); } } } } 运行测试 首先启动服务端程序,然后运行客户端程序,可以看到客户端输出 Remote Service Version: My Sample Service - v0.0.1.19 你好 gRPC Hello gRPC Hola gRPC Bonjour gRPC こんにちは gRPC hallo gRPC OK,大功告成!
简介 Hprose(High Performance Remote Object Service Engine)是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,而且功能强大。 也是一个跨语言的RPC框架,但由于库的质量参差不齐,一些语言的库并不完善。这里以C#为例来实现一个简单的服务端和客户端程序。 创建项目 新建解决方案,包含两个项目 server 控制台程序,服务端 client 控制台程序,客户端 然后,通过Nuget分别为两个项目安装Hprose.RPC库。 这里并不需要再创建额外的服务接口项目,只需要手动定义一个接口即可。 创建服务接口 在server和client项目下都新建一个接口,作为服务接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // IHello.cs public class ServiceVersion { public string Name { get; set; } public string Version { get; set; } public ServiceVersion() { } public ServiceVersion(string name, string ver) { Name = name; Version = ver; } } // 服务接口 public interface IHello { ServiceVersion GetVersion(); List<string> SayHello(string name); } IHello里面的两个接口函数就是服务接口了。 服务端程序 依旧是服务端实现接口,客户端来调用,在server项目下新建类Hello.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // Hello.cs namespace server { using System.Collections.Generic; public class Hello : IHello { public ServiceVersion GetVersion() { return new ServiceVersion("Hello Service", "0.0.1.21"); } public List<string> SayHello(string name) { return new List<string>() { $"你好 {name}", $"Hello {name}", $"Hola {name}", $"Bonjour {name}", $"こんにちは {name}", $"hallo {name}" }; } } } 编写服务启动程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace server { using Hprose.RPC; using System.Net; class Program { static void Main() { HttpListener server = new HttpListener(); server.Prefixes.Add("http://localhost:10240/"); server.Start(); Service service = new Service().Bind(server).AddInstanceMethods(new Hello()); System.Console.WriteLine("Server listening at http://localhost:10240/ \n Press any key exit ..."); System.Console.ReadKey(); server.Stop(); } } } 客户端程序 由于client项目刚刚也定义了IHello接口,这里就可以直接调用接口函数了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 namespace client { using Hprose.RPC; class Program { static void Main() { Client cli = new Client("http://localhost:10240/"); IHello hello = cli.UseService<IHello>(); ServiceVersion ver = hello.GetVersion(); System.Console.WriteLine("Remote Service Version: {0} - v{1}", ver.Name, ver.Version); var hellos = hello.SayHello("Hprose"); foreach (string item in hellos) { System.Console.WriteLine(item); } System.Console.ReadKey(); } } } 运行测试 先启动服务端程序,再启动客户端程序,可以看到客户端输出 Remote Service Version: Hello Service - v0.0.1.21 你好 Hprose Hello Hprose Hola Hprose Bonjour Hprose こんにちは Hprose hallo Hprose 与Thrift和gRPC相比,Hprose实现起来要简单很多,暂时还没有尝试跨语言调用,不知道是不是同样简单。
简介 Inno Setup是一个免费的安装包生成软件,完全开源免费,使用起来也非常方便,文档也十分全面。与其它同类软件相比十分的小巧便携,功能也十分全面。 近期Inno Setup的6.1.0版本也即将发布,也带来了更多的新功能。由于正式版本还没有发布,这里就使用的先行版本。 下载页面 Inno Setup 6.1版本新增了安装过程中的下载页面,在所有选项准备完毕,正式开始安装之前可以下载需要的文件。官方也给出了下载示例的代码 CodeDownloadFiles.iss。 [Code] var DownloadPage: TDownloadWizardPage; function OnDownloadProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean; begin if Progress = ProgressMax then Log(Format('Successfully downloaded file to {tmp}: %s', [FileName])); Result := True; end; procedure InitializeWizard; begin DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadProgress); end; function NextButtonClick(CurPageID: Integer): Boolean; begin if CurPageID = wpReady then begin DownloadPage.Clear; DownloadPage.Add('https://files.jrsoftware.org/is/6/innosetup-6.1.0-dev.exe', 'innosetup-6.1.0-dev.exe', ''); DownloadPage.Add('https://jrsoftware.org/download.php/iscrypt.dll', 'ISCrypt.dll', '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc'); DownloadPage.Show; try try DownloadPage.Download; Result := True; except SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK); Result := False; end; finally DownloadPage.Hide; end; end else Result := True; end; 上面代码在初始化时创建了一个下载页面,并在wpReady之后显示。 CreateDownloadPage的原型: function CreateDownloadPage(const ACaption, ADescription: String; const OnDownloadProgress: TOnDownloadProgress): TDownloadWizardPage; 创建一个下载页面用于下载文件并显示进度。 前两个参数指定页面的标题和页面描述,第3个参数是在下载进度更新后的回调函数TOnDownloadProgress,可以指定为nil(空)。 TOnDownloadProgress = function(const Url, FileName: string; const Progress, ProgressMax: Int64): Boolean; CreateDownloadPage返回TDownloadWizardPage类型: TDownloadWizardPage = class(TOutputProgressWizardPage) property AbortButton: TNewButton; read; procedure Add(const Url, BaseName, RequiredSHA256OfFile: String); procedure Clear; function Download: Int64; end; 可以看到,TDownloadWizardPage有一个Add的方法,用于新增一个下载任务,它有3个参数: Url:下载链接,BaseName:下载后的文件名称 RequiredSHA256OfFile:文件的哈希值,用于校验下载文件,值为空时,则忽略校验 Clear方法清空下载任务列表,Download方法开始下载任务。 具体的使用可以看上面的NextButtonClick函数里的写法。 另一个方法是使用DownloadTemporaryFile函数: function DownloadTemporaryFile(const Url, FileName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64; [Code] function OnDownloadProgress(const Url, Filename: string; const Progress, ProgressMax: Int64): Boolean; begin if ProgressMax <> 0 then Log(Format(' %d of %d bytes done.', [Progress, ProgressMax])) else Log(Format(' %d bytes done.', [Progress])); Result := True; end; function InitializeSetup: Boolean; begin try DownloadTemporaryFile('https://jrsoftware.org/download.php/is.exe', 'innosetup-latest.exe', '', @OnDownloadProgress); DownloadTemporaryFile('https://jrsoftware.org/download.php/iscrypt.dll', 'ISCrypt.dll', '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc', @OnDownloadProgress); Result := True; except Log(GetExceptionMessage); Result := False; end; end; 使用起来和前一种方法有所不同,但大致都是类似的,这里不再赘述。 消息框设计器 软件的工具菜单(Tools)中新增了消息框设计器(MessageBox Designer)工具。 工具提供了两种消息框,Message Box和Task Dialog Message Box,工具可以设置对话框的图标,按钮和默认选项等。 将鼠标指针放在需要插入对话框的代码位置,打开MessageBox Designer,完成选项后点击OK即可,然后就可以看到先前鼠标所在的位置插入了一段MessageBox代码。 1 2 3 [CustomMessages] DownloadComplete=下载完成 DownloadCompleteMessage=下载已完成。 // Display a message box SuppressibleTaskDialogMsgBox(CustomMessage('DownloadComplete'), CustomMessage('DownloadCompleteMessage'), mbInformation, MB_OK, ['OK'], 0, IDOK); 链接 Inno Setup 6 Revision History Inno Setup 简体中文翻译
前言 今天偶然看到一个新的开源的git服务软件Gitea,一看到界面,瞬间就爱了,因为之前我自己用的是gitblit,界面比较简单,主要是用来管理公司的一些小项目。今天看到Gitea之后,就决定迁移到过去,简单折腾了一下,配置起来比gitblit要简单一些,但界面却更加漂亮了,总体上看起来比较像github,并且还支持主题系统,很合我的胃口。 下载二进制包 首先去下载对应系统的二进制包,可以去github或者官网下载最新的发布版本。 我是在windows下配置的,所以选择下载windows版的可执行程序。 开启服务 下载后不需要安装,直接就能运行,但直接运行的话会有一个控制台显示在桌面,所以可以考虑将程序作为一个系统服务在后台运行。 由于不需要安装,可以直接将下载的可执行程序放在自己想要安装的目录,eg: D:\gitea\gitea.exe 以管理员方式打开cmd或者powershell,执行命令: 1 sc create gitea start= auto binPath= "\"D:\gitea\gitea.exe\" web --config \"D:\gitea\custom\conf\app.ini\"" 打开系统的服务管理界面,找到gitea,此时是已停止状态,按下鼠标右键,在弹出的菜单选择开始,然后可以看到状态变为正在启动。 打开浏览器访问 http://localhost:3000,应该就能看到Gitea的界面了,点击页面上的探索(explore),还需要进行一些配置才能正常运行,数据库可以直接使用sqlite,这样就不需要再安装其它的数据库了,然后就是一些目录配置,根据需要选择目录就行。 最后可以创建管理员账号,也可以不用设置,完成安装后第一个注册的账号会自动成为管理员。 到这里安装和配置就完成了,此时再看服务中的gitea的状态,已经变为正在运行。 迁移仓库到 Gitea Gitea 自带迁移外部仓库的功能,但我一直导入失败,提示没有导入本地仓库的权限。后来只能将本地的仓库推送到Gitea,不过效果是一样的。 首先在Gitea中新建一个同名的仓库,复制仓库的git链接。 修改本地仓库的origin 1 $ git remote origin set-url http://localhost:3000/user/myrepo.git 或者直接修改.git/config文件中的origin url。 推送本地仓库到Gitea 1 $ git push origin main 静静等待推送完成,然后刷新gitea的仓库页面,就可以看到所有的提交记录了。 启用 SSH 编辑 D:\gitea\custom\conf\app.ini,在[server]部分新增一项配置 1 2 [server] START_SSH_SERVER = true 保存后,在服务中重启 gitea 即可。 在gitea的账号管理中新增SSH密钥,然后就可以使用ssh的方式管理账户所拥有的仓库了。 最后 总的来看,Gitea的配置十分简单,基本上下载后就可以使用,没有其它的依赖,gitblit则需要安装java运行环境,界面十分漂亮,功能也比较完善,自用完全没有问题。
前言 由于换了新的工作,我的工作方向也有了很大的变化,之前基本上是单纯的写代码,现在则经常需要和硬件设备交互,开发平台也转到了Linux+Qt。硬件设备的控制,其中最基本的就是LED灯以及一些开关继电器的操作,其本质就是GPIO的操作。考虑到系统的精简和成本控制,最好是可以直接通过Linux系统去控制,当然也有其它替代方案,比如使用支持Modbus协议的IO模块。关于Modbus的使用,后面有空再讲,这里就记录一下最简单的Linux系统下的GPIO控制,用户空间下的GPIO文件系统接口。 在此之前,有必要再了解一下GPIO的概念。 “通用输入/输出”(GPIO)是一种灵活的软件控制数字信号。它们由多种芯片提供,对于使用嵌入式和定制硬件的Linux开发人员来说很熟悉。每个GPIO代表一个连接到特定引脚的位,即球栅阵列(BGA)封装上的“球”。电路板示意图显示了哪些外部硬件连接到哪些GPIO。驱动程序可以通用地编写,以便板设置代码将这样的引脚配置数据传递给驱动程序。 A “General Purpose Input/Output” (GPIO) is a flexible software-controlled digital signal. They are provided from many kinds of chip, and are familiar to Linux developers working with embedded and custom hardware. Each GPIO represents a bit connected to a particular pin, or “ball” on Ball Grid Array (BGA) packages. Board schematics show which external hardware connects to which GPIOs. Drivers can be written generically, so that board setup code passes such pin configuration data to drivers. 在单片机上,我们可以很方便的控制GPIO,但在嵌入式Linux上则不一样,通常GPIO对于用户来说是不可见的,不过Linux系统也提供了相应的接口供用户控制GPIO,每个非专用的引脚都可以用作GPIO。 配置IO多路复用器(IOMUXC) 将需要复用的IO添加到pinctrl_hog节点,例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /* kernel/arch/arm64/boot/dts/freescale/OK8MP-C.dts */ &iomuxc { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_hog>; pinctrl_hog: hoggrp { fsl,pins = < MX8MP_IOMUXC_HDMI_DDC_SCL__HDMIMIX_HDMI_SCL 0x400001c3 MX8MP_IOMUXC_HDMI_DDC_SDA__HDMIMIX_HDMI_SDA 0x400001c3 MX8MP_IOMUXC_HDMI_HPD__HDMIMIX_HDMI_HPD 0x40000019 MX8MP_IOMUXC_HDMI_CEC__HDMIMIX_HDMI_CEC 0x40000019 /* GPIO */ MX8MP_IOMUXC_GPIO1_IO07__GPIO1_IO07 0x159 MX8MP_IOMUXC_GPIO1_IO09__GPIO1_IO09 0x159 MX8MP_IOMUXC_GPIO1_IO12__GPIO1_IO12 0x159 MX8MP_IOMUXC_ECSPI2_MOSI__GPIO5_IO11 0x159 MX8MP_IOMUXC_ECSPI2_MISO__GPIO5_IO12 0x159 MX8MP_IOMUXC_ECSPI2_SS0__GPIO5_IO13 0x159 >; }; } 具体的GPIO名字要参照xxxx-pinfunc.h里面的定义,配置为GPIO时,一定要使用IOMUXC_xxxx_xxxx__GPIOn_IOxx的宏定义。 然后是后面的上下拉配置,具体的计算方法和参数意义如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 PAD_CTL_HYS (1 << 16) /* Hysteresis 滞后使能*/ PAD_CTL_PUS_100K_DOWN (0 << 14) /* 100KOhm Pull Down */ PAD_CTL_PUS_47K_UP (1 << 14) /* 47KOhm Pull Up */ PAD_CTL_PUS_100K_UP (2 << 14) /* 100KOhm Pull Up */ PAD_CTL_PUS_22K_UP (3 << 14) /* 22KOhm Pull Up */ PAD_CTL_PUE (1 << 13) /* Pull / Keep Enable */ PAD_CTL_PKE (1 << 12) /* Pull / Keep Select 0: Keeper 1: Pull */ PAD_CTL_ODE (1 << 11) /* Open Drain Enable 漏极开路 */ PAD_CTL_SPEED_LOW (1 << 6) /* 带宽配置 */ PAD_CTL_SPEED_MED (2 << 6) PAD_CTL_SPEED_HIGH (3 << 6) PAD_CTL_DSE_DISABLE (0 << 3) /* Drive Strength Field 驱动能力 */ PAD_CTL_DSE_240ohm (1 << 3) PAD_CTL_DSE_120ohm (2 << 3) PAD_CTL_DSE_80ohm (3 << 3) PAD_CTL_DSE_60ohm (4 << 3) PAD_CTL_DSE_48ohm (5 << 3) PAD_CTL_DSE_40ohm (6 << 3) PAD_CTL_DSE_34ohm (7 << 3) PAD_CTL_SRE_FAST (1 << 0) /* Slew Rate Field 压摆率 */ PAD_CTL_SRE_SLOW (0 << 0) 注意:不要直接设置为0,没有任何作用。可以使用0x80000000,它表示“我不知道,保持默认值”。 用户空间下的GPIO读写操作 用户空间下的GPIO文件系统接口在/sys/class/gpio/目录下。 注意:以下操作都需要root权限! 使能GPIO 在此之前,需要先知道GPIO对应的编号数值,计算方法如下: GPIOn_IOx = (n - 1) × 32 + x 例:GPIO5_IO13 = (5 - 1) × 32 + 13 = 141 执行命令 1 2 3 # echo N > /sys/class/gpio/export # N为GPIO对应的编号,例: echo 141 > /sys/class/gpio/export 如果需要取消使能GPIO,则执行命令 1 2 3 # echo N > /sys/class/gpio/unexport # N为GPIO对应的编号,例: echo 141 > /sys/class/gpio/unexport GPIO配置 使能GPIO之后,/sys/class/gpio/目录下就多出来了相应的GPIO节点目录。例:gpio141 GPIO节点有以下属性可以配置: /sys/class/gpio/gpioN/ direction 读取为in或者out。通常可以写入此值。写入out默认输出为低。为了确保操作无误,可以写入值“low”和“high”将GPIO配置为具有该初始值的输出。 请注意,如果内核不支持更改GPIO的方向,或者该属性是由内核代码导出的,而内核代码没有明确允许用户空间重新配置该GPIO方向,则该属性将不存在。 value 读取为0(low)或1(high)。如果GPIO被配置为输出,则可以写入该值;任何非零值都被视为高。 如果引脚可以配置为中断生成,并且它已经配置为生成中断(请参阅“edge”的描述),那么您可以对该文件进行轮询(poll),每当触发中断时,轮询(poll)将返回。如果使用poll,请设置事件为POLLPRI和POLLERR。如果使用select,则将文件描述符设置为exceptfds。轮询返回后,要么lseek到sysfs文件的开头并读取新值,要么关闭文件并重新打开以读取值。 edge 读取为none、rising、falling或者both。编写这些字符串以选择将对“value”文件返回进行轮询的信号边缘。 仅当引脚可以配置为中断生成输入引脚时,此属性才存在。 active_low 读取为0(假)或1(真)。写入任何非零值以反转读取和写入的值属性。现有和后续轮询支持通过边缘属性配置“rising”和“falling”边缘将遵循此设置。 例: 将GPIO配置为输出 1 echo out > /sys/class/gpio/gpio{N}/direction 将GPIO配置为输入,上升沿触发中断 1 2 echo in > /sys/class/gpio/gpio{N}/direction echo rising > /sys/class/gpio/gpio{N}/edge GPIO读写 GPIO配置为输入(in)时,只能读取输入值,不能写入 GPIO配置为输出(out)时,可以读取当前值和写入新值 1 2 3 4 5 # 读取时 cat /sys/class/gpio/gpio{N}/value # 写入时 echo 0 > /sys/class/gpio/gpio{N}/value echo 1 > /sys/class/gpio/gpio{N}/value 程序控制GPIO 示例读取: 1 2 3 4 5 6 7 8 9 10 11 int readGpio(unsigned short io) { QFile file(QString("/sys/class/gpio/gpio%1/value").arg(io)); if (!file.open(QIODevice::ReadOnly)) return 0; QByteArray ba = file.readAll(); file.close(); return QString(ba).toInt(); } 示例写入: 1 2 3 4 5 6 bool writeGpio(unsigned short io, unsigned char value) { char buf[128]; sprintf(buf, "echo %d > /sys/class/gpio/gpio%d/value", value, io); return ::system(buf) == 0; } 参考 Legacy GPIO Interfaces GPIO Sysfs Interface for Userspace Definitive GPIO guide
标题 1 2 3 4 5 6 # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题 也可以使用闭合方式的标题,结尾的#可以不必和开头一致 1 2 3 # 一级标题 # ## 二级标题 ## ... 另一种方式 1 2 3 4 5 一级标题 ======= 二级标题 ------- 当然也可以用HTML的方式 1 2 3 4 5 6 <h1>一级标题</h1> <h2>二级标题</h2> <h3>三级标题</h3> <h4>四级标题</h4> <h5>五级标题</h5> <h6>六级标题</h6> HTML的好处在于可以方便的使标题居中 1 <h1 align="center">居中标题</h1> 目录 可以使用[TOC]标记来自动生成目录,但兼容性貌似不怎么好 1 [TOC] 分隔线 可以使用3个以上的*、-作为分隔线,中间也可以插入空格 1 2 3 4 *** * * * --- - - - 字体 粗体 在需要以粗体显示的文字前后各加两个*或_可以使文字加粗显示 1 2 **粗体** __粗体__ 斜体 在需要以斜体显示的文字前后各加一个*或_可以使文字已斜体显示 1 2 *斜体* _斜体_ 删除线 在文字前后各加两个~可以在文字上添加删除线 1 ~~删除线~~ 当然也可以进行组合使用 1 2 ***斜体加粗*** __~~粗体删除线~~__ 颜色 在写作过程中可能会遇到不少情况需要将文字用不同颜色标注,可以使用HTML的方式来实现,同时也可以设置字体和大小 1 <font face="微软雅黑" color=red size=12>落霞与孤鹜齐飞,秋水共长天一色。</font> 段落 Markdown的换行有些奇特,直接Enter换行它好像不认,需要在段落结尾加两个空格+换行才可以,或者在上一段落和下一段落之间再加一行空行,即两次换行也可以。 落霞与孤鹜齐飞,秋水共长天一色。 渔舟唱晚,响穷彭蠡之滨; 雁阵惊寒,声断衡阳之浦。 1 2 3 4 落霞与孤鹜齐飞,秋水共长天一色。 渔舟唱晚,响穷彭蠡之滨; 雁阵惊寒,声断衡阳之浦。 引用 写在>后的文字即可显示为引用,引用可以嵌套使用 1 2 3 > 引用的文字 >> 嵌套引用的文字 >>> 更多嵌套 表格 表头 表头 表头 表头 内容 居左 居中 居右 1 2 3 |表头|表头|表头|表头| |---|:--|:--:|---:| |内容|居左|居中|居右| 第一行是表头,第二行代表对齐方式,默认是居左,在-左边加:即可居左对齐,在-右边加:可居右对齐,两边都加:表示居中对齐 列表 有序列表 1 2 3 1. 列表1 2. 列表2 3. 列表3 无序列表 可以使用*、+或者-作为标记 1 2 3 * 列表1 + 列表2 - 列表3 任务列表 @mentions, #refs, links, formatting, and tags supported list syntax required (any unordered or ordered list supported) this is a complete item this is an incomplete item 1 2 - [x] 已完成的任务 - [ ] 未完成的任务 链接 可以直接输入网址,如:https://github.com/ 或者使用格式:[Text](url) 点击这里返回主页 1 点击[这里](https://kira-96.github.io/)返回主页 也可以使用HTML的方式 1 点击<a href="https://kira-96.github.io/" target="_blank">这里</a>返回主页 还有一种就是使用索引的方式 例:谷歌、百度 1 2 3 4 例:[谷歌][1]、[百度][2] [1]: https://www.google.com.hk/ "google" [2]: https://www.baidu.com/ "百度" 锚 主要用于在页面内跳转 点击这里查看链接的用法 1 点击[这里](#链接)查看链接的用法 图片 图片和链接的格式很像,url可以使用相对位置和绝对位置,当然网络位置也可以 ![Alt Text](url) 1 ![图片](https://image-url.jpg) 也可以使用HTML的方式 1 <img src="https://image-url.jpg" width="50%" height="50%"> 设置对齐方式 1 2 3 <div align=center> <img src="https://image-url.jpg" width="50%" height="50%"> </div> 标注 这个用的并不多,看起来像是课本上文言文里面那种注释的感觉 例: 滕王阁序的作者是王勃1。 1 2 3 滕王阁序的作者是王勃[^1]。 [^1]: 王勃(约650——676年),唐代诗人。汉族,字子安。绛州龙门(今山西河津)人。王勃与杨炯、卢照邻、骆宾王齐名,世称“初唐四杰”,其中王勃是“初唐四杰”之首。 行内代码 可以直接使用两个`(反引号)包裹行内代码 例:我们学习的第一行代码通常都是printf("Hello World!")。 1 我们学习的第一行代码通常都是`printf("Hello World!")`。 语法高亮 1 2 3 4 5 int main(void) { printf("Hello World!\n"); return 0; } 1 2 3 4 5 6 7 ``` cpp int main(void) { printf("Hello World!\n"); return 0; } ``` 公式 公式对于写论文的同学来说是非常有用的,Markdown的公式也比word的公式编辑方便多了。 行内公式,使用$ $包括在内。 如:$e=mc^2$ 1 $e=mc^2$ 单行公式,公式会单独占用一行,使用$$ $$包括在内。 $$Fe+CuSO_4=FeSO_4+Cu$$ 1 $$Fe+CuSO_4=FeSO_4+Cu$$ 其中具体的符号和字母之类的需要的时候可以到网上去找,如Markdown 数学公式。 转义字符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \\ 反斜杠 \` 反引号 \* 星号 \_ 下划线 \{\} 大括号 \[\] 中括号 \(\) 小括号 \# 井号 \+ 加号 \- 减号 \. 英文句号 \! 感叹号 注释 可以使用HTML的注释方式,会在生成的HTML中以注释的形式存在,不显示出来。 1 或者使用 1 2 3 <div style='display: none'> 我是注释内容 </div> 😃 Emoji 🎉 Markdown甚至支持Emoji 😍😜😠💢😷👿😈💕 Emoji Cheat Sheet 写在最后 自从接触了Markdown之后,我就很少使用Word这类工具了。日常工作和生活中用它来写文档和笔记真的是相当舒服,语法简单好记,完全可以满足需求,使用起来方便快捷,还可以借助HTML来实现一些比较复杂的功能。 不过我们公司内部似乎没什么人使用,可能是由于我们公司并不是互联网企业,所以没有那么潮流,感觉可以借机会安利一波,对于提高整体的工作效率也有不小的帮助。 王勃(约650——676年),唐代诗人。汉族,字子安。绛州龙门(今山西河津)人。王勃与杨炯、卢照邻、骆宾王齐名,世称“初唐四杰”,其中王勃是“初唐四杰”之首。 ↩︎
简介 JSON是一种常用的轻量级数据交换格式。与XML相比,JSON无论是体积还是可读性都更好,所以在网络数据传输和应用程序中被广泛的应用。 那么,.NET平台使用最广泛的JSON库是什么呢?自然要数Newtonsoft.NET了,打开nuget包管理器第一个就是,在所有包下载量排行中排名第一。使用简单,性能可靠,文档也很齐全。 使用 使用JSON最常用的就是对象的序列化和反序列化。 先来看最基本的使用 1 2 3 4 5 6 7 // 先定义一个类 public class TestJsonDeseClass { public Guid MessageGuid { get; set; } public string Message { get; set; } } 1 2 3 4 5 6 7 8 TestJsonDeseClass test = new TestJsonDeseClass() { MessageGuid = Guid.NewGuid(), Message = "Test Message" }; string json = JsonConvert.SerializeObject(test); TestJsonDeseClass des = JsonConvert.DeserializeObject<TestJsonDeseClass>(json); 只需要将类的成员属性设置为get和set就可以了,反序列化的时候,Json.NET会自动根据成员的名字为对象的成员赋值。 那么如果不想序列化/反序列化某个成员变量呢? 1 2 3 4 5 6 7 8 9 using Newtonsoft.Json; public class TestJsonDeseClass { [JsonIgnore] public Guid MessageGuid { get; set; } public string Message { get; set; } } 只需要在成员变量的定义前加上[JsonIgnore]的属性(Attribute)即可,序列化/反序列化的时候Json.NET会自动忽略该成员。 如果JSON字符串中的属性名字和定义的类中的成员名字不一样怎么办呢?怎样才能正确的给成员变量赋值呢? 1 2 3 4 5 6 7 8 9 using Newtonsoft.Json; public class TestJsonDeseClass { [JsonProperty("Guid")] public Guid MessageGuid { get; set; } public string Message { get; set; } } 只需要在成员变量的定义前加上[JsonProperty()]的属性(Attribute)即可,序列化/反序列化的时候Json.NET会将Json字符串中的"Guid"属性赋值给MessageGuid。 那么,如果想让类的属性值只读的get,不想让外部能修改成员变量呢,如何设置呢? 当然这样也是可以的,不过需要我们给类添加构造方法,在构造方法中对成员赋值,不能再使用默认的构造方法,因为默认的构造方法不会对成员赋值,而外部也无法对成员赋值。在添加了构造方法后,Json.NET会自动调用类的构造方法。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 using Newtonsoft.Json; public class TestJsonDeseClass { public Guid MessageGuid { get; } public string Message { get; } public TestJsonDeseClass(Guid messageGuid, string message) { MessageGuid = messageGuid; Message = message; } } 不过需要注意的是,构造方法的参数名称必须和成员变量(或者说是序列化/反序列化时的属性)名字一致,但可以不用区分大小写,才能正确对属性赋值。 如果把上面的构造方法改成下面这个样子 1 2 3 4 5 public TestJsonDeseClass(Guid guid, string message) { MessageGuid = guid; Message = message; } 就会导致反序列化的对象MessageGuid属性不能正确赋值,因为Json.NET无法从json字符串中找到名为guid的属性,你也没告诉它要拿名为MessageGuid的属性,自然就会出错了。 那么,最后一个问题,如果我的类有多个构造方法,我怎样告诉Json.NET应该用哪一个呢? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using Newtonsoft.Json; public class TestJsonDeseClass { public Guid MessageGuid { get; } public string Message { get; } public TestJsonDeseClass() { } [JsonConstructor] public TestJsonDeseClass(Guid messageGuid, string message) { MessageGuid = messageGuid; Message = message; } } 只需要在对应的构造方法前面加上[JsonConstructor]的属性(Attribute)即可,反序列化的时候Json.NET会就会调用相应的构造方法来生成对象了。 以上就是一些基本的用法,基本上能满足正常的使用了。当然还有一些更加高级和灵活的用法,这里就不多记录了,需要的时候再去看文档就可以了。 参考 文档 Samples
简介 Prism是一个用于WPF、Xamarin Forms、WinUI等的MVVM框架,刚刚学习,这里只是个人总结的一些知识点笔记。 IoC IContainerProvider 1 2 3 4 protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } 1 2 3 4 5 6 public void OnInitialized(IContainerProvider containerProvider) { var regionManager = containerProvider.Resolve<IRegionManager>(); var viewA = containerProvider.Resolve<ViewA>(); ... } IContainerRegistry 1 2 3 4 5 6 7 8 9 // App.xaml.cs protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.Register<IApplicationCommands, ApplicationCommands>(); containerRegistry.RegisterDialog<NotificationDialog, NotificationDialogViewModel>(); containerRegistry.RegisterForNavigation<Page1>(); containerRegistry.RegisterForNavigation<Page2>(); ... } Module IModule 1 2 3 4 5 6 7 8 9 10 public class SimpleModule : IModule { public void OnInitialized(IContainerProvider containerProvider) { } public void RegisterTypes(IContainerRegistry containerRegistry) { } } 使用App.config加载模块 1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="utf-8"?>
文档 The Rust Programming Language Second edition Rust 程序设计语言(第二版)简体中文版 Rust by Example 通过例子学 Rust Async programming in Rust with async-std async-std 中文文档 其它 Rust Language Cheat Sheet Rust Fundamentals Rust语言中文社区
简介 Stylet是一个轻量且功能强大的MVVM框架。支持 .NET 4.5+ 和 .NET Core 3.0+。 Stylet的作者也是受到Caliburn.Micro的启发,并且在CM的基础上做了许多改进。所以Stylet使用起来感觉和Caliburn.Micro差别不是很大,但又有着一些不同。 项目结构 这里选择创建一个 .NET Core 的 WPF 项目。 这里项目结构风格和Caliburn.Micro类似,示例源代码 虽然Stylet官方给出的例子里面View和ViewModel是放在一起的,但经过实际使用后发现采用CM的风格也是可以的。依照习惯,将Views和ViewModels分别放在两个文件夹中。 使用 Bootstrapper.cs 1 2 public class Bootstrapper : Bootstrapper<ShellViewModel> {} 这样就相当于执行了DisplayRootViewFor()。 然后再修改App.xaml如下就可以让程序启动了。 App.xaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14
简介 Thrift是由Facebook为“大规模跨语言服务开发”而开发的一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。目前被作为一个RPC框架使用。 下载 使用之前需要先下载Thrift的源代码和Thrift编译器。 下载源代码后解压,进入到thrift-0.12.0\lib\csharp\src目录下,打开Thrift.sln,根据需要编译相应的库,这里选择Thrift.45,即.NET 4.5可以使用的库,编译生成Thrift45.dll。 创建项目 新建解决方案,包含3个项目 ThriftSample 类库,Thrift生成的服务接口 server 控制台程序,服务端 client 控制台程序,客户端 这3个项目都需要引用刚刚编译的Thrift45.dll,为什么不用Nuget来安装Thrift呢?因为我注意到Nuget上的Thrift已经好几年没更新了,还是手动编译最新的要好。 然后,server和client同时引用项目ThriftSample。 定义服务接口 在解决方案目录下新建一个文件Sample.thrift来定义服务接口,Thrift语法可以在网上找到 1 2 3 4 5 6 7 8 9 10 11 namespace csharp kira.Interface service SampleService { ServiceVersion GetVersion() list<string> SayHello(1: string name) } struct ServiceVersion { 1: required string name; 2: required string version; } 这里指定了生成类的命名空间,以及定义了一个结构体作为返回值 生成服务接口 这里就需要用到之前下载的Thrift编译器,可以直接在Thrift官网找到。将下载的thrift-0.12.0.exe也拷贝到解决方案目录下(和Sample.thrift相同目录),打开命令窗口,执行以下命令 1 $ thrift-0.12.0.exe -gen csharp Sample.thrift 不得不说,Thrift的命令行语法真的比gRPC简洁多了。 执行完没有错误的话,就可以看到目录下又多出了一个gen-csharp的文件夹,里面有对应命名空间的文件夹,最后找到生成的.cs文件。将文件全部拷贝到ThriftSample项目目录下,并将它们添加到项目,编译生成库。 服务端程序 在server项目下新建类MySampleService并实现接口SampleService.Iface,重写其中的两个服务接口函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 namespace server { using System.Collections.Generic; using kira.Interface; public class MySampleService : SampleService.Iface { public ServiceVersion GetVersion() { return new ServiceVersion() { Name = "My Sample Service", Version = "0.0.1.20" }; } public List<string> SayHello(string name) { return new List<string>() { $"你好 {name}", $"Hello {name}", $"Hola {name}", $"Bonjour {name}", $"こんにちは {name}", $"hallo {name}" }; } } } 编写服务启动程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace server { using Thrift.Server; using Thrift.Transport; using kira.Interface; class Program { static void Main(string[] args) { MySampleService service = new MySampleService(); SampleService.Processor processor = new SampleService.Processor(service); TServerTransport serverTransport = new TServerSocket(10240); TServer server = new TThreadPoolServer(processor, serverTransport); System.Console.WriteLine("server listening at tcp://localhost:10240/"); server.Serve(); } } } 客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 namespace client { using Thrift.Protocol; using Thrift.Transport; using kira.Interface; class Program { static void Main(string[] args) { TTransport transport = new TSocket("localhost", 10240); transport.Open(); TProtocol protocol = new TBinaryProtocol(transport); SampleService.Client cli = new SampleService.Client(protocol); ServiceVersion ver = cli.GetVersion(); System.Console.WriteLine("Remote Service Version: {0} - v{1}", ver.Name, ver.Version); var hellos = cli.SayHello("Thrift"); foreach (string item in hellos) { System.Console.WriteLine(item); } System.Console.ReadKey(); transport.Close(); } } } 运行测试 老样子,先启动服务端程序,然后运行客户端程序,可以看到客户端输出 Remote Service Version: My Sample Service - v0.0.1.20 你好 Thrift Hello Thrift Hola Thrift Bonjour Thrift こんにちは Thrift hallo Thrift 总结 对比Thrift和gRPC两个主流的RPC框架,个人感觉Thrift使用起来要更加灵活一些,当然这只是初步接触,较深层次的内容还没有去研究,实际项目的应用感觉两个都可以,有对比说Thrift框架的性能要优于gRPC,但对于小的项目来说已经完全够用了。
虽然平时很少会用到命令行参数,但有时候可以使用命令行参数来使程序执行不同的行为。 在写控制台程序的时候,我们可以直接得到程序命令行参数。 1 2 3 4 static void Main(string[] args) { // args 就是命令行参数 } 那么如果不是控制台程序如何获取命令行参数呢? 在 WPF 中有两种方法获取命令行参数 第一种方法是重写应用的OnStartup方法,通过StartupEventArgs来获取命令行参数。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // App.xaml.cs /// /// App.xaml 的交互逻辑 /// public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 获取命令行参数 string[] args = e.Args; // do something } } 如果没有命令行参数,那么args就为null。 第二种方法则比较灵活,可以在任意地方获取到命令行参数。 1 string[] args = System.Environment.GetCommandLineArgs(); 直接使用Environment的静态方法来获取命令行参数,需要注意的是,第二种方法获取到的参数和前面一种方法结果不同。 第二种方法获取的结果不会为null,通过Environment获取到的命令行参数第一个是当前程序的路径,从第2项开始才是命令行参数(如果有)。
进程间传递数据的方法 在进程间传递数据也就意味着两个不同的应用程序之间的通讯,大家可能会想到使用消息队列(Message Queue)来作为解决方案,当然这可能是最优解,然而这里我要讲的是另外一种方法,通过Windows的消息机制来传递数据,内容比较硬核。 依旧用到了两个Windows的API,FindWindow和SendMessage,以及WPF如何和MFC窗口通讯,可以参考上一篇文章。 传递数据的方式 这里需要先知道一个Window消息WM_COPYDATA 它在WinUser.h中的定义如下 1 #define WM_COPYDATA 0x004A 这个消息就是这次要讲的内容,通过这个消息就可以在不同的窗口间传递数据了,它有两个参数,WPARAM是发送消息窗口的句柄,LPARAM是一个结构体的指针,这个结构体在WinUser.h里定义如下: 1 2 3 4 5 6 7 8 /* * lParam of WM_COPYDATA message points to... */ typedef struct tagCOPYDATASTRUCT { ULONG_PTR dwData; DWORD cbData; _Field_size_bytes_(cbData) PVOID lpData; } COPYDATASTRUCT, *PCOPYDATASTRUCT; 嗯…看起来也不是很复杂,为了能够正确处理这个指针,需要在C#中也定义一个相同的结构体 1 2 3 4 5 6 7 8 9 10 11 12 /// /// COPYDATASTRUCT /// 对应 C++ 里的 COPYDATASTRUCT /// 不能更改 /// [StructLayout(LayoutKind.Sequential)] public struct COPYDATASTRUCT { public IntPtr dwData; //可以是任意值 public int cbData; //指定lpData内存区域的字节数 public IntPtr lpData; //发送给目录窗口所在进程的数据 } 这样就可以了,其中最重要的就是lpData,它可以是任意对象的指针,只要C++和C#两边定义了一个相同的结构体,我们就可以用它来直接传递结构体对象😮,这种方式比通过消息队列传递数据要快得多。 那么先来定义一个简单的结构体吧: 1 2 3 4 5 6 7 8 9 10 11 12 // C++ struct StructUser { char UserName[32]; char Password[32]; StructUser() { ZeroMemory(UserName, 32); ZeroMemory(Password, 32); } }; 1 2 3 4 5 6 7 8 9 // C# [StructLayout(LayoutKind.Sequential)] public struct StructUser { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string UserName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string Password; } 这里定义了一个User的结构体,UserName和Password都是长度为32的字符串,注意C#中结构体的定义方式,这里指定了字符串的长度,就是为了和C++的结构体保持一致。 先来看看C++中如何发送和处理WM_COPYDATA消息的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 // 发送 StructUser user; char userName[32]; char password[32]; // GetDlgItem(IDC_EDIT_USERNAME)->GetWindowTextA(userName, 32); // GetDlgItem(IDC_EDIT_PASSWORD)->GetWindowTextA(password, 32); memcpy(user.UserName, userName, 32); memcpy(user.Password, password, 32); COPYDATASTRUCT copyData; copyData.dwData = 0; copyData.lpData = &user; copyData.cbData = sizeof(StructUser); HWND hWnd = nullptr; if (m_pTargerWnd == nullptr) { hWnd = ::FindWindow(nullptr, "WpfWindow"); } else { hWnd = m_pTargerWnd->GetSafeHwnd(); } ::SendMessage(hWnd, WM_COPYDATA, (WPARAM)GetSafeHwnd(), (LPARAM)& copyData); // 接收 /* C++ 中相关代码 * 处理 WM_COPYDATA 消息 * Header File(.h) --------------------------------------------------------------------- ... afx_msg BOOL OnCopyData(CWnd *pWnd, COPYDATASTRUCT *pCopyDataStruct); ... DECLARE_MESSAGE_MAP() --------------------------------------------------------------------- * Source File(.cpp) BEGIN_MESSAGE_MAP(CxxxDlg, CDialogEx) ... ON_WM_COPYDATA() ... END_MESSAGE_MAP() ... */ BOOL CxxDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) { if (pWnd != nullptr) { m_pTargerWnd = pWnd; } if (pCopyDataStruct != nullptr) { StructUser* pUser = (StructUser*)(pCopyDataStruct->lpData); // DWORD dwLen = pCopyDataStruct->cbData; // GetDlgItem(IDC_EDIT_USERNAME)->SetWindowTextA(pUser->UserName); // GetDlgItem(IDC_EDIT_PASSWORD)->SetWindowTextA(pUser->Password); } return CDialogEx::OnCopyData(pWnd, pCopyDataStruct); } C#会比较复杂一些 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 // 消息处理函数 private IntPtr WndProcFunc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { switch (msg) { case WM_COPYDATA: // public const int WM_COPYDATA = 0x004A; IntPrt hWnd_Target = wParam; COPYDATASTRUCT param = Marshal.PtrToStructure<COPYDATASTRUCT>(lParam); StructUser user = Marshal.PtrToStructure<StructUser>(param.lpData); // UserName.Text = user.UserName; // Password.Text = user.Password; break; default: break; } return IntPtr.Zero; } // 发送消息 StructUser sctUser = new StructUser() { UserName = UserName.Text, Password = Password.Text }; IntPtr userPtr = Marshal.AllocHGlobal(Marshal.SizeOf<StructUser>()); Marshal.StructureToPtr<StructUser>(sctUser, userPtr, true); COPYDATASTRUCT copyData = new COPYDATASTRUCT() { dwData = IntPtr.Zero, cbData = Marshal.SizeOf<StructUser>(), lpData = userPtr, }; IntPtr copyDataPtr = Marshal.AllocHGlobal(Marshal.SizeOf<COPYDATASTRUCT>()); Marshal.StructureToPtr<COPYDATASTRUCT>(copyData, copyDataPtr, true); if (hWnd_Target == IntPtr.Zero) hWnd_Target = Win32Api.FindWindow(null, "MfcWindow"); hWnd_MainWnd = new WindowInteropHelper(this).Handle; if (hWnd_Target != IntPtr.Zero) Win32Api.SendMessage(hWnd_Target, WM_COPYDATA, hWnd_MainWnd, copyDataPtr); Marshal.FreeHGlobal(userPtr); // Marshal.FreeHGlobal(copyDataPtr); // 最后一定要释放掉非托管内存 处理消息的地方和上一篇一样,发送的地方比较复杂,总体来讲就是用Marshal开辟了两块非托管的内存来存放两个结构体的数据,等到消息被处理之后再将非托管内存释放掉,避免内存泄漏。 那么,这次的内容就到这里了,主要内容都是代码,但其实也不复杂,只是需要慢慢消化。
发送消息到指定窗口 发送消息相对来说比较简单,这里先讲,这里需要用到两个Windows的API 1 2 3 4 5 6 7 8 9 10 11 12 // 查找指定窗口 [DllImport("User32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Auto)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); //消息发送API [DllImport("User32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] public static extern int SendMessage( IntPtr hWnd, // 信息发往的窗口的句柄 int Msg, // 消息ID IntPtr wParam, // 参数1 IntPtr lParam // 参数2 ); FindWindow就是根据窗口的名字去找到相应的窗口句柄,SendMessage就是发送消息到指定窗口了,写过MFC的应该不陌生 1 2 3 4 IntPtr hWnd = FindWindow(null, windowName); if (hWnd == IntPtr.Zero) return; SendMessage(hWnd, WM_USER + 2, IntPtr.Zero, "msg"); 发送消息就讲这么多,剩下的需要自行摸索,下面是用WPF窗口接收消息 获取WPF窗口句柄 Windows消息是通过窗口句柄来传递给指定窗口的,所以想要处理WPF窗口的收到消息,首先就需要获取自身的窗口句柄。 1 HwndSource hWnd = PresentationSource.FromVisual(this) as HwndSource; 或者 1 2 IntPtr hwnd = new WindowInteropHelper(this).Handle; HwndSource source = HwndSource.FromHwnd(hwnd); 这里的this就是WPF的窗口,当然也可以通过这种方式获取窗口任意控件的句柄 添加钩子(AddHook) 得到窗口的句柄之后就可以为该窗口添加钩子来处理窗口收到的消息了 1 2 3 4 5 6 7 8 9 10 11 12 protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); // Add Hook // HwndSource source = PresentationSource.FromVisual(this) as HwndSource; // source.AddHook(WndProcFunc); // 或者 HwndSource.FromHwnd(new WindowInteropHelper(this).Handle).AddHook(new HwndSourceHook(WndProcFunc)); } 或者 1 2 3 4 5 6 7 public MainWindow() { InitializeComponent(); this.SourceInitialized += (s, e) => { HwndSource.FromHwnd(new WindowInteropHelper(this).Handle).AddHook(new HwndSourceHook(WndProcFunc)); }; } 这个WndProcFunc就是我们的消息处理函数了,窗口在收到消息之后就会走到这个函数里面,它看起来像是这样 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { // Handle Msg Here switch (msg) { case WM_USER + 1: // 你的消息值 { // 处理 wParam, lParam break; } // ... 其它消息 default: break; } return IntPtr.Zero; } 这样就完了吗?是的,但是!目前还不能处理传递的参数,如果WPF程序和C++程序处于一个进程还好说,如果是两个进程,那么他们之间的内存是不共用的,所以即使WPF窗口拿到了指针也读不出指针里的内容。 那么要怎样才能在WPF程序和C++程序之间传递值呢,有空在讲…
问题描述 在使用 WSL 更新软件包的时候经常会遇到这样一个报错 1 /sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link 意思是说 /usr/lib/wsl/lib/libcuda.so.1 不是一个符号链接。 问题分析 通过名字可以判断这应该是nVidia显卡驱动相关的库,进入 /usr/lib/wsl/lib/ 目录,可以看到有 libcuda.so、libcuda.so.1、libcuda.so.1.1 三个文件,都是文件形式,而通过报错我们知道 libcuda.so、libcuda.so.1 应该是符号链接文件。 它们关系应该是: libcuda.so -> libcuda.so.1 -> libcuda.so.1.1 知道原因就好解决了,把 libcuda.so、libcuda.so.1 删掉,再重新创建符号链接就可以了。 1 2 ubuntu@dell:/usr/lib/wsl/lib$ sudo rm libcuda.so rm: 无法删除 'libcuda.so': 只读文件系统 很遗憾,这样是不行的。最后经过多方查找,终于找到了解决方案。 解决方法 解决方法就是上面的方法,但不是在 WSL 中操作。 使用管理员权限执行 cmd 命令: 1 2 3 4 5 C:>cd C:\Windows\System32\lxss\lib C:\Windows\System32\lxss\lib>del /s /q "libcuda.so" C:\Windows\System32\lxss\lib>del /s /q "libcuda.so.1" C:\Windows\System32\lxss\lib>mklink libcuda.so.1 libcuda.so.1.1 C:\Windows\System32\lxss\lib>mklink libcuda.so libcuda.so.1 或者在Powershell中执行: 1 2 3 4 5 6 cd C:\Windows\System32\lxss\lib rm libcuda.so rm libcuda.so.1 wsl -e /bin/bash ln -s libcuda.so.1.1 libcuda.so.1 ln -s libcuda.so.1.1 libcuda.so 然后在 wsl 中执行: 1 $ sudo ldconfig 参考 ldconfig: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link libcuda.so.1 is not a symbolic link #5548
前言 由于我很少用MFC,只有工作上需要的时候才会用到,所以我也是个新手,遇到问题需要到网上找很久资料。这里只是记录一些特殊情景下会用到的技巧,方便以后查找。 隐藏窗口任务栏图标 这个问题我在网上找了很久,大致有两种方案: 修改窗口的扩展样式 1 ModifyStyleEx(WS_EX_APPWINDOW,WS_EX_TOOLWINDOW); 但是这样做会导致整个窗口的样式会变得很难看。 将一个隐藏窗口设置成主窗口的父窗口 这样做比较麻烦,而且任务视图下也不能再看到窗口,显然不是我想要的效果。 最后终于找到了一个比较完美的解决方案,通过COM的方式移除任务栏列表中的图标。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 显示/隐藏任务栏图标(COM方式) bool ShowInTaskbar(HWND hWnd, bool isShow) { CoInitialize(nullptr); ITaskbarList* pTaskbarList; HRESULT hr = CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskbarList, (void**)&pTaskbarList); if (SUCCEEDED(hr)) { pTaskbarList->HrInit(); if (isShow) { pTaskbarList->AddTab(hWnd); } else { pTaskbarList->DeleteTab(hWnd); } CoUninitialize(); return true; } CoUninitialize(); return false; } 程序启动时默认隐藏窗口 一种比较简单的方法是在程序启动时将窗口移动到屏幕外的不可见区域。 如果直接在OnInitDialog中设置ShowWindow(SW_HIDE)是无效的,因为此时窗口还没有显示出来,自然也无法隐藏。 这里的思路是在程序启动的时候先将窗口移动到屏幕外,然后通过另一个线程将窗口隐藏起来,这样做虽然程序启动后窗口还是会一闪即逝,但由于是在屏幕之外,实际上并不能看到,然后在窗口需要显示的时候调用ShowWindow(SW_SHOW)即可。 1 2 3 4 5 6 7 8 9 10 11 12 // CxxxDlg.h 头文件 #include class CxxxDlg : public CDialogEx { ... ... ... private: std::future<int> hideTask; // 后台隐藏窗口线程 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 BOOL CxxxDlg::OnInitDialog() { ... CRect rcClient; GetWindowRect(&rcClient); // 将窗口移动到屏幕外 MoveWindow(-rcClient.Width(), rcClient.top, rcClient.Width(), rcClient.Height()); // 新建线程,延时1s后隐藏窗口 hideTask = std::async( std::launch::async, [&] { std::this_thread::sleep_for(std::chrono::seconds(1)); // ShowWindow(SW_HIDE); ShowWindowAsync(m_hWnd, SW_HIDE); std::this_thread::sleep_for(std::chrono::seconds(1)); // 延时1s,待窗口完全隐藏后再将窗口居中 CenterWindow(); return 0; }); ... return TRUE; // 除非将焦点设置到控件,否则返回 TRUE } 点击关闭时隐藏窗口 有时候我们希望在点击主窗口的关闭按钮之后将窗口隐藏或者最小化,而不是退出程序。这时只需要拦截掉窗口的关闭消息即可。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // CxxxDlg.h 头文件 class CxxxDlg : public CDialogEx { ... protected: afx_msg void OnSysCommand(UINT nID, LPARAM lParam); ... } void CxxxDlg::OnSysCommand(UINT nID, LPARAM lParam) { ///////////////////////////////////// // 这里捕获窗口的关闭消息 // 不直接关闭窗口,而是隐藏起来 if (nID == SC_CLOSE) { ShowWindowAsync(m_hWnd, SW_HIDE); } ///////////////////////////////////// else { CDialogEx::OnSysCommand(nID, lParam); } } 这样就可以拦截掉窗口的关闭消息了,这样不仅仅是点击关闭按钮时会隐藏窗口,通过窗口菜单关闭窗口或是使用Alt+F4都不能真正关闭窗口。那么我怎样才能退出程序呢,总不能用任务管理器吧。 其实很简单,在需要退出程序的时候向窗口发送WM_CLOSE消息即可。 1 SendMessage(WM_CLOSE); CFileDialog导致CDialogEx“失去焦点”的解决方法 继承自CDialogEx的窗口在使用CFileDialog之后会导致窗口标题栏变成灰色,很像是窗口失去了焦点,此时窗口仍然能够正常操作,但即使鼠标点击在窗口上窗口的标题栏依旧是灰色,无法恢复到窗口激活状态的颜色,即使将窗口属性设置为WS_EX_TOPMOST(置顶)依旧是这样,必须点击窗口外的其它区域或者使用Tab切换一下才能恢复正常。 而继承自CDialog的窗口则没有这个问题,可以将窗口的基类改成CDialog来避免这个问题。对于我这样的强迫症来说是无法忍受的,所以用尽千方百计终于找到了一个可行的解决方案。 经过尝试,单纯让窗口获取焦点或者将窗口放到前台的方法都是无效的。 1 2 3 4 5 6 7 8 9 10 11 12 CFileDialog dlg( TRUE, _T("ini"), nullptr, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _T("ini(*.ini)|*.ini|TEXT(*.txt)|*.txt|"), this); auto dlgResult = dlg.DoModal(); // 无效的方法 this->SetFocus(); this->SetForegroundWindow(); ... 所以只能曲线救国,既然通过手动切换的方式可以恢复到正常状态,不妨先切换到其它窗口再切换回来。 1 2 3 4 5 6 7 8 9 10 11 12 CFileDialog dlg( TRUE, _T("ini"), nullptr, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _T("ini(*.ini)|*.ini|TEXT(*.txt)|*.txt|"), this); auto dlgResult = dlg.DoModal(); // 先将焦点放到桌面,再切换回本窗口 ::SetForegroundWindow(::GetDesktopWindow()); this->SetForegroundWindow(); ... 完美解决了问题。 窗口启用视觉样式 启用视觉样式之后,可以让程序看起来更加现代化一些,只支持Window XP以后的系统。 可以通过为程序添加清单文件来实现,不过比较麻烦。在VC++ 2005之后,直接添加编译器指令到代码中就可以了。 在预编译头文件中添加下面代码即可。 1 2 3 4 5 6 7 8 9 #if defined _M_IX86 #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") #elif defined _M_IA64 #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df' language='*'\"") #elif defined _M_X64 #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") #else #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") #endif 任务栏显示进度 为窗口的任务栏图标添加进度显示,也可以为任务栏添加按钮。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ITaskbarList3* pTaskbar; // 初始化COM组件 CoInitialize(NULL); CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pTaskbar)); // TBPF_NOPROGRESS = 0, // 正常状态,不显示进度 // TBPF_INDETERMINATE = 0x1, // 忙碌状态,不显示进度 // TBPF_NORMAL = 0x2, // 正常状态,显示进度(绿色) // TBPF_ERROR = 0x4, // 错误状态,显示进度(红色) // TBPF_PAUSED = 0x8 // 停止状态,显示进度(黄色) pTaskbar->SetProgressState(GetSafeHwnd(), TBPF_NORMAL); pTaskbar->SetProgressValue(GetSafeHwnd(), 60, 100); // 设置提示信息 pTaskbar->SetThumbnailTooltip(GetSafeHwnd(), TEXT("Tooltip")); // 设置覆盖图标 HICON hIcon = AfxGetApp()->LoadIcon(IDI_ICON_ERR); pTaskbar->SetOverlayIcon(GetSafeHwnd(), hIcon, _T("Error")); // 添加任务栏按钮 THUMBBUTTONMASK dwMask = THB_ICON | THB_TOOLTIP; THUMBBUTTON buttons[3]; buttons[0].iId = 0; buttons[0].dwMask = dwMask; buttons[0].hIcon = hIcon; memcpy(buttons[0].szTip, TEXT("Tooltip"), sizeof(buttons[0].szTip)); // ... pTaskbar->ThumbBarAddButtons(GetSafeHwnd(), 3, buttons); // 最后释放COM组件 CoUninitialize(); 未完待续,持续更新中… 参考 MFC简单的启动时隐藏界面方式(仅启动时隐藏) Enabling Visual Styles
说明 这里是个人工作时常用的一些git命令,现在越来越多了,小本本都快记不下了,这里稍微做一下整理。 工具下载 首先是git的下载地址: 官网:https://git-scm.com/ taobao镜像:https://npm.taobao.org/mirrors/git-for-windows/ 由于官网的下载速度很慢,推荐使用taobao镜像的下载地址。 设置用户名和邮箱 全局配置 1 2 $ git config --global user.name [name] $ git config --global user.email [email] 配置当前仓库 1 2 $ git config user.name [name] $ git config user.email [email] 查看用户名和邮箱 1 2 $ git config user.name $ git config user.email 生成SSH密钥 1 $ ssh-keygen -t rsa -C "[email]" 执行完毕和用户目录下就会生成一个**.ssh**文件夹。 拉取远程代码库 可以直接clone远程代码仓库(推荐) 1 $ git clone https://github.com/libgit2/libgit2 mylibgit 先初始化仓库,再拉取代码 1 2 3 4 5 $ cd ./mylibgit $ git init $ git remote add origin git@github.com:libgit2/libgit2.git $ git pull origin master $ git push -u origin master 提交代码变更 查看工作区变更 1 $ git status 添加文件到暂存区 1 $ git add README.md 添加所有变更文件到暂存区 1 $ git add . 提交到本地仓库 1 $ git commit -m 'Update README' 推送到远程代码仓库(main是分支) 1 $ git push origin main 版本切换 查看版本/提交记录 1 $ git log 查看版本/提交简介 1 $ git log --pertty=oneline 撤销提交,保留代码变更 撤销上次提交 1 $ git reset --soft HEAD^ 撤销前n次提交 1 $ git reset --soft HEAD~n 回退到某次提交记录 1 $ git reset --soft commit-id 版本回退,不保留代码变更 回退到当前最新提交 1 $ git reset --hard HEAD 回退到上一版本 1 $ git reset --hard HEAD^ 回退到之前的第n个版本 1 $ git reset --hard HEAD~n 回退到某个版本/重新切换回未来版本 1 $ git reset --hard commit-id 强制推送:在已经推送到远程的记录又被修改的情况下 1 $ git push origin main --force 1 $ git push -f origin main 分支管理 查看当前仓库分支 1 $ git branch 查看当前仓库以及远程仓库所有分支 1 $ git branch -a 在当前分支的基础上创建新分支 1 $ git checkout -b 分支名 删除已合并的本地分支 1 $ git branch -d 分支名 删除未合并的本地分支 1 $ git branch -D 分支名 删除远程仓库分支 1 $ git push origin -d 分支名 或 1 $ git push origin :分支名 删除远程已经删除的分支 1 $ git remote prune origin 标签-tag tag和分支操作类似 查看tag 1 $ git tag 给当前版本添加tag 1 $ git tag 标签名 给某一版本添加tag 1 $ git tag 标签名 commit-id 删除标签 1 $ git tag -d 标签名 删除远程标签 1 $ git push origin -d 标签名 推送标签到远程仓库 1 $ git push origin 标签名 子模块(submodule) 当前仓库添加子模块 1 $ git submodule add 拉取仓库后拉取子模块 1 2 $ git submodule init $ git submodule update 1 $ git submodule update --init 或 1 $ git clone --recurse-submodules 更新子模块 1 $ git submodule update --remote 删除子模块 1 2 $ git submodule deinit $ git rm --cached 保持fork之后的仓库和上游同步 遇到一些好的代码仓库,有时候会fork一份到自己的账号,但一旦原来的代码仓库有了新的提交,如何保持自己的仓库和源仓库代码同步呢? 一种方式是通过远程仓库向fork之后的仓库提交一个PR,但这样会导致提交记录不一致,非常EP,在网上搜罗了好久,终于找到一个完美的方法。 首先需要将fork之后的仓库clone到本地。 然后设置本地仓库的上游仓库地址为源仓库 1 $ git remote add upstream git@github.com:kira-96/myblog.git 同步上游仓库变更 1 2 3 $ git fetch upstream $ git checkout main $ git merge upstream/main 推送到远程仓库 1 $ git push origin main 参考 git思维导图 保持fork之后的项目和上游同步
前言 由于我是从事医疗行业软件开发的,所以必不可少的会和图像打交道,最近刚刚好在做一个图像旋转相关的功能,借此又复习(预习)了一下线性代数,趁着没忘赶紧做一下笔记。 DICOM 中与方位计算有关的 Tag 在开始之前,有必要先了解一下DICOM中与图像方位计算有关的几个Tag,主要有3个。 Tag Keyword (0020,0032) Image Position (Patient) (0020,0037) Image Orientation (Patient) 其中Image Position指的是图像左上角的像素在患者坐标系中的位置。 Image Orientation由6个数字组成,分别是图像的行(Row)方向和列(Column)方向的单位向量与x/y/z坐标轴夹角的余弦值(cosine)。 有了上面两个Tag的值,就可以计算出图像在空间坐标系中的位置和方位了。 计算法向量 现在我们已经有了图像平面上两个垂直的向量,行和列方向的向量,使用行列式就能计算出图像所在平面的法向量了。 $$u × v = \left[\begin{matrix} i & j & k \\ u_1 & u_2 & u_3 \\ v_1 & v_2 & v_3 \end{matrix}\right]$$ 具体怎么算,可以看这里,讲得很详细。 代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 double[] vector1 = new[] {orientation[0], orientation[1], orientation[2]}; double[] vector2 = new[] {orientation[3], orientation[4], orientation[5]}; double[] normal = new double[] { 0, 0, 0 }; normal[0] = vector1[1] * vector2[2] - vector1[2] * vector2[1]; normal[1] = vector1[2] * vector2[0] - vector1[0] * vector2[2]; normal[2] = vector1[0] * vector2[1] - vector1[1] * vector2[0]; var temp = Math.Sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]); normal[0] /= temp; normal[1] /= temp; normal[2] /= temp; 其中orientation就是Image Orientation中的6个值。计算得到的normal就是图像所在平面的法向量的单位向量。 图像方位矩阵 在3D图形变换中经常使用的是4维矩阵,把图像的位置信息也放到矩阵中,可以方便的进行位置变换的计算。 这里分别用u,v,w表示图像的行/列/法线方向向量,S代表图像位置。 $$u=(u_1,u_2,u_3)$$ $$v=(v_1,v_2,v_3)$$ $$w=(w_1,w_2,w_3)$$ $$S=(s_x,s_y,s_z)$$ 4维矩阵则表示为 $$matrix=\left[\begin{matrix} u_1 & v_1 & w_1 & s_x \\ u_2 & v_2 & w_2 & s_y \\ u_3 & v_3 & w_3 & s_z \\ 0 & 0 & 0 & 1 \end{matrix}\right]$$ 图像旋转 先来看特殊情况下的旋转,即矩阵绕坐标轴的旋转。T为变换矩阵。 绕X轴旋转 $$T=\left[\begin{matrix} 1 & 0 & 0 & 0 \\ 0 & cos\theta & -sin\theta & 0 \\ 0 & sin\theta & cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{matrix}\right]$$ 绕Y轴旋转 $$T=\left[\begin{matrix} cos\theta & 0 & sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -sin\theta & 0 & cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{matrix}\right]$$ 绕Z轴旋转 $$T=\left[\begin{matrix} cos\theta & -sin\theta & 0 & 0 \\ sin\theta & cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{matrix}\right]$$ 直接用$matrix \times T$就可以得到旋转后的矩阵了。 但是图像旋转并不一定是绕坐标轴旋转,这里说图像旋转指的是在图像所在平面上的旋转,即图像矩阵绕法线旋转一定角度,不存在其它情况,所以需要一种更加通用的计算方法。角度的正负按右手定则决定。 经测试,下面的方法并不通用,下面的旋转矩阵适用于点位置的变换,不适用于DICOM中的方位变换 这里直接给出结果,图像矩阵绕向量$(u,v,w)$旋转$\theta$的变换矩阵T。 $$T=\left[\begin{matrix} u^2+(1-u^2)cos\theta & u v(1-cos\theta)-w sin\theta & u w(1-cos\theta)+v sin\theta & 0 \\ u v(1-cos\theta)+w sin\theta & v^2+(1-v^2)cos\theta & v w(1-cos\theta)-u sin\theta & 0 \\ u w(1-cos\theta)-v sin\theta & v w(1-cos\theta)+u sin\theta & w^2+(1-w^2)cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{matrix}\right]$$ 代码示例: 这里用到了MatrixD,主要是用于矩阵的运算。 这里其实是我想的复杂了,DICOM图像旋转的本质就是两个方向向量的旋转,只需要将两个方向向量绕法向量旋转即可。而这一切fo-dicom都已经为我们做好了。 1 2 3 4 5 6 7 8 9 10 Vector3D forward = new Vector3D(new[] { orientation[0], orientation[1], orientation[2] }); Vector3D down = new Vector3D(new[] { orientation[3], orientation[4], orientation[5] }); Orientation3D orientation3D = new Orientation3D(forward, down); // 旋转,顺时针为正,逆时针为负 orientation3D.Pitch(angle * Math.PI / 180.0); // orientation3D.Forward // orientation3D.Down 实现起来很简单,浏览Pitch的源码就会发现,其实就是将两个向量绕Right向量(即法向量)旋转,得到新的Forward和Down就是旋转后图像的方位信息。 这里借一张图来说明问题: 在创建Orientation3D时的参数Forward和Down就是图像的行方向和列方向的方向向量,Pitch方法将Forward和Down向量旋转一个角度,得到的就是旋转后图像的行和列的方向向量。 图像翻转 翻转后的方位计算则比较简单,如果是水平翻转,只需要将水平(Row)方向的向量反向即可,竖直翻转将竖直(Column)方向的向量反向即可。 向量反向只需要将 (u,v,w) 3个值前面添加负号即可。 1 2 3 4 5 6 7 8 9 10 11 12 // 水平翻转 var newOrientation = new double[6] { -orientation[0], -orientation[1], -orientation[2], orientation[3], orientation[4], orientation[5] }; // 竖直翻转 var newOrientation = new double[6] { orientation[0], orientation[1], orientation[2], -orientation[3], -orientation[4], -orientation[5] }; 不过,像这么奇葩的功能应该不会有人去用吧。 注意 对图像矩阵进行旋转或者翻转操作之后,由于图像左上角的像素已经发生变化,所以原有的位置信息也已经改变,需要重新计算才能保证图像在空间中处于正确的位置。对于图像翻转来说或许能够轻易计算出来,不过旋转之后的图像却比较难计算了。 最后,附上一段计算图像方位的代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // [R] Right - 沿着X方向递减 // [L] Left - 沿着X方向递增 // [A] Anterior - 沿着Y方向递减 // [P] Posterior - 沿着Y方向递增 // [F] Feet - 沿着Z方向递减 // [H] Head - 沿着Z方向递增 static string ComputeOrientation(Vector3D vector) { char x = vector.X < 0 ? 'R' : 'L'; char y = vector.Y < 0 ? 'A' : 'P'; char z = vector.Z < 0 ? 'F' : 'H'; double x1 = Math.Abs(vector.X); double y1 = Math.Abs(vector.Y); double z1 = Math.Abs(vector.Z); string result = ""; for (int i = 0; i < 3; i++) { if (x1 > 0.0001 && x1 > y1 && x1 > z1) { result += x; x1 = 0; } else if (y1 > 0.0001 && y1 > x1 && y1 > z1) { result += y; y1 = 0; } else if (z1 > 0.0001 && z1 > x1 && z1 > y1) { result += z; z1 = 0; } else { break; } } return result; } 这里用到了Vector3D,表示一个3维向量,可以使用数组代替。 参考 知乎:如何理解线性代数? 行列式,快速求出法向量 MRI的DICOM图像方位算法的研究 三维空间几何变换矩阵 图形学 位移,旋转,缩放矩阵变换 DICOM中几个判断图像方向的tag DICOM Standard Browser