华为触屏笔记本经常触屏失灵的一种解决方法

现象

前些年做项目,原来的thinkpad X240不够用了,就用赚的钱换了个华为的14寸的触屏笔记本,开始时一直没啥问题,去年下半年发现,触屏经常不好用,重启就好,但是我个人不喜欢重启电脑。后面发现用华为电脑管家修复问题,有机率修复好,但最近又不行了。

近期发现,当触屏不灵的时候,设备管理器中有一个I2C的设备显示错误,并且,设备管理器中没有触屏这个设备,而我禁用这个有问题的I2C设备,再启用,它就好了,触屏设备也出现了。

分析原因

触屏是通过I2C总线连接到cpu上的,但是当电脑休眠再唤醒后,这个设备不能正常加载,导致触屏失灵。既然可以通过设备管理器来禁用再启用就解决,那是不是可以通过命令行来实现自动化一键完成呢?

解决办法

有了上面的分析,就在豆包上问了几轮。

第一轮,我问在win平台上有没有办法用命令行来禁用设备,它给了三种方法,一种是用sc config,这个是对驱动是以服务形式加载的可以用,而I2C设备不有对应的服务。第二种是用Powershell命令,我想试一下,但是发现我不知道设备的标识符要怎么看,然后就问豆包。第三种是用一个devcon的工具,我没有安装,就暂时不考虑了。

第二轮,怎么在win平台上查看设备的InstanceId ,它给出了方法,我照着方法果然找到了。下面是它给的方法

1
Get-PnpDevice | Where-Object {$_.FriendlyName -like "*I2C*"}

找到这个ID后,就测试了下Disable-PnpDevice -InstanceId "设备的InstanceId" -Confirm:$false命令,果然有效。又试了下Enable-PnpDevice -InstanceId "设备的InstanceId" -Confirm:$false,也实现了目的。

第三轮,怎么把Disable-PnpDevice -InstanceId "设备的InstanceId" -Confirm:$false完成后延时5s再enable这个设备,写成一个可以执行的脚本。豆包正确的识别了我的意图,输出了三行powershell的命令,然后我把命令保存为ps1文件,再在powershell的终端界面运行,果然可行。

然后就是想办法让它可以在系统解锁的时候自动执行。

还是用豆包,查到了怎么把写的ps1文件填加到计划任务中去。

在 “程序或脚本” 文本框中输入 “powershell.exe”,在 “添加参数” 文本框中输入要执行的.ps1 脚本的路径及文件名,例如 “-File C:\Scripts\yourscript.ps1”。

GNOME-内存管理

以下内容译自:https://developer.gnome.org/documentation/guidelines/programming/memory-management.html 加入了个人的一些理解。

GNOME是的堆栈主是是用C编写的,所以动态的内存分配都要手动管理。可以使用Glib中提供的API来实现,但在编代码时要时刻记得内存。

这里假定读者都知道内存使用、堆分配,Glib中与malloc(), free(),相类似的是 g_malloc(), g_free().

前提知识-程序内存分配

摘自 https://blog.csdn.net/yingms/article/details/53188974

一个由C/C++编译的程序占用的内存分为以下几个部分

1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放

4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放

5、程序代码区—存放函数体的二进制代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//main.c
int a = 0; //全局初始化区
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main() {
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来的10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

栈 stack:

由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间

堆 heap:

需要程序员自己申请,并指明大小,在C中malloc函数

如p1 = (char *)malloc(10);

在C++中用new运算符

如p2 = (char *)malloc(10);

但是注意p1、p2本身是在栈中的。

内存管理原则

这部分原文写的比较绕,简单的说,就是GNOME提供了一种内存所有权的机制来管理内存。每块分配的内存,只有一个所有者,所有权可以转移,只有所有者有权释放内存。

这里有一个重要的限制:变量绝不能从 owned 更改 更改为 unowned(反之亦然)。此限制是简化的关键。

1
2
3
char *  generate_string (const char *template);
/*这里定义的参数是用const修饰的,表示函数在调用的时候,不会把参数的所有权转进来*/
void print_string (const char *str);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//从修饰符可以看出变量具不具有内存所有权
char *my_str = NULL; /* owned 表示在栈里,退出时需要释放 char*修饰的 */
const char *template; /* unowned 表示不在栈里,因为这是const修饰的*/
GValue value = G_VALUE_INIT; /* owned GValue修饰的 */
g_value_init (&value, G_TYPE_STRING);

/* Transfers ownership of a string from the function to the variable. */
template = "XXXXXX";
my_str = generate_string (template); //template的所有权从系统管理到my_str了,因为template原本就没有所有权
/* template是用const修饰的,generate_string的参数也是const修饰的,他们本身都没有内存的所有权 */

/* No ownership transfer. */
print_string (my_str); //没有发生所有权转移

/* Transfer ownership. We no longer have to free @my_str. */
//这里发生所有权转移,猜测my_str的形参类型定义的是gchar,没有const修饰
g_value_take_string (&value, my_str);

/* We still have ownership of @value, so free it before it goes out of scope. */
g_value_unset (&value);

有一些库中用一些词来表示内存所有权的转移,比如函数名中用take表示完全转移,例如g_value_take_string()

理想情况是,所有的函数都注释了所有相关的参数和返回值是否有转移所有权,下面有几个方法可以确定是否有所有权转移。

  1. 如果这个类型有注释,认真看注释里怎么说的
  2. 否则,如果是const修饰的类型,没有转移
  3. 否则,如果函数文档明确的说明了返回值必须要释放,这里有或全部转移了
  4. 否则,如果函数名中有dup,take,steal,这里有或全部转移了
  5. 否则,如果函数名中有peek,没有转移
  6. 否则,你需要看一下函数的代码来确认是否发生转移

有了这些方法,就可以方便的判断内存所有权和转移情况了。注意,copy()函数必须使用正确的数据类型,比如g_strdup()是针对string的,g_object_ref()是针对GObject的。

论起内存所有权转移,malloc()/free() 与 引用计数是等价的。前者,一个新分配的堆内存转移,后者,一个新的引用(转移)。

函数文档

正确的在文档中记录每个函数和参数以及返回值的所有权转移是很重要的。最好的记录所有权转移的方法,是用(transfer) 记录,这是在gobject-introspection里介绍的方法。在API文档中包含了这个注释后,自检工具就可以读到了。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* g_value_take_string:
* @value: (transfer none): an initialized #GValue
* @str: (transfer full): string to set it to
*
* Function documentation goes here.
*/

/**
* generate_string:
* @template: (transfer none): a template to follow when generating the string
*
* Function documentation goes here.
*
* Returns: (transfer full): a newly generated string
*/

变量的内存所有权可以在代码中通过行内注释来记录。这个不是标准化的,任何工具也都读不到, 用好了,形成一个规范,也很方便。

1
2
GObject *some_owned_object = NULL;  /* owned */
GObject *some_unowned_object; /* unowned */

在文档中记录容器类型也是一种规范,它包括了所包含元素的类型。如

1
2
3
4
5
6
7
8
/* PtrArray<owned char*> */
GPtrArray *some_unowned_string_array; /* unowned */

/* PtrArray<owned char*> */
GPtrArray *some_owned_string_array = NULL; /* owned */

/* PtrArray<owned GObject*> */
GPtrArray *some_owned_object_array = NULL; /* owned */

需要注意,有所有权的变量,都应该被初始化,这样释放的时候会更方便。

引用计数

GLib库除了提供类似malloc()/free()这样的常规内存管理类型外,还包含多种引用计数类型的实现,其中GObject是一个典型的例子。所有权和转让的概念同样适用于引用计数类型,正如它们适用于已分配的类型一样。当一个作用域通过调用g_object_ref()持有对实例的强引用时,即表示该作用域拥有这个引用计数类型。通过再次调用g_object_ref()可以实现实例的“复制”(增加引用计数)。而释放所有权则是通过调用g_object_unref()来完成的——尽管这一步可能并不会立即导致实例被销毁,但它确实释放了当前作用域对该实例的所有权。

在GLib中,还有其他支持引用计数的数据类型,例如 GHashTable(使用 g_hash_table_ref() 和 g_hash_table_unref())或 GVariant(使用 g_variant_ref() 和 g_variant_unref())。由于历史原因,一些类型,如 GHashTable,既支持引用计数也支持显式终结。应始终优先使用引用计数,因为它允许实例在多个作用域之间轻松共享(每个作用域持有自己的引用),而无需为实例分配多个副本。这样可以节省内存。

浮动引用

ssh免密登陆

ssh免密登陆并不是什么复杂的事,但是配置了以后,会很方便。为了实现这个,在网上找了一些教程,也不知是服务器的默认配置的问题,还是什么原因,一直没有成功。

今天又试了下,在大语言模型智谱清言的帮助下,实现了免密登陆。记录一下。

客户端生成密钥

命令行中输入ssh-keygen -t rsa,然后一路回车,可以在当前用户的文件夹下的.ssh目录里生成两个文件,id_rsa.pub和 id_rsa。id_rsa.pub是公钥,可以给别人的,另一个是私钥,只能在自己电脑上的。

客户端复制公钥到服务端

如果是windows系统,这时就最好是用winscp一类的工具软件连接服务器,linux系统,可以用ssh-cp-id命令。

我是用的windows,下面说下怎么操作。

首先ssh登陆远程服务器,然后cd到当前用户下的.ssh目录里,ls查下有无authorized_keys这个文件,如果没有,使用touch authorized_keys创建这个文件。

然后,nano authorized_keys打开这个文件,也可以用vi。

第三步,用记事本打开客户端用户文件夹下.ssh目录中的id_rsa.pub文件。

第四步,复制id_rsa.pub中的全部内容。

第五步,到nano authorized_keys的编辑界面,右键,自动会把刚刚复制的内容粘贴到编辑界面中。

第六步,ctrl+o,再回车,保存文件。

第七步,ctrl+x,退出编辑。

修改服务端文件权限

服务器上~/.ssh/authorized_keys文件的权限是600,使用chmod 600 ~/.ssh/authorized_keys修改。

修改服务端ssh配置文件

这步可以在客户端的ssh界面操作。

配置文件通常是/etc/ssh/sshd_config,确保下面的配置是这样的。

1
2
3
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

重启sshd,service sshd restart

非22端口的操作

对于22端口的,上面的操作基本就完成了,再用ssh连的时候,ssh user@ip,就可以登陆了,对于非22端口的,再记端口,麻烦,可以用config文件来搞定。

在客户端的.ssh目录下,新建一个config文件,不要后缀。

在里面写上下面这些行

1
2
3
4
5
Host ssh连接的名称
User 用户名
Port 端口号
HostName 服务端的域名或ip地址
IdentityFile id_rsa

大功告成。

编程基础-变量、地址与指针

地址

前面咱们说了,数据在存储过程中,为了方便,将存储介质分成了不同的区域和格子。为了方便的找到数据,又给每个存储的格子,也就是存储单元,分配了地址。但是这些地址是一长串数,不好记,怎么办?起个名字吧,这个名字,就是变量名。为什么叫变量名呢?因为这个地址里的数据,大多数时候,是可以修改的,也就是可以变化的,所以叫变量名。

变量

说是变量,其实有些数据在运行的时候是不变的,那些就叫常量,常量可以说是变量的一种特例吧。变量其实就是内存中数据的一个别名,方便是记忆和使用的。知道了这个名字,就知道这个数据在哪里(地址),也就能找到数据值了。注意,这里的数据不指定具体的类型。

变量类型

但是,就像数学中,有整数,小数,实数,复数等等类型一样,计算机中,数据也是有类型的。但是,前面不是说了么,计算机中数据存的时候只是0或1这样存的,怎么表示不同的类型呢?这其实中间要有一个翻译的过程。比如0b 0010,如果说它是整数类型,也就整型,那它就表示3,为什么是3呢?因为0010是二进制,转成十进制,就是3。那如果说它是字符型,它是什么字符呢?可以查下ascii码表,查表可知,它表示ETX这个特殊符号。

所以,变量类型存在的意义,就是把存储单元中存储的二进制数据转变成实际数据时用到的一个翻译选项,不同的变量类型,在翻译的时候要用不同的翻译方法。

指针

那指针,又是什么东西呢?指针,其实就是地址。它,可以是任意类型数据的地址。这就厉害了,任意类型的数据都有指针,所以,指针也有指针,或者说,地址也有地址,就好像你家有一个地址,但是这个地址在物业那里有一个通讯录,通讯录里有你家的地址,你家的地址在通讯录的某页某行处。你家,可以看成是一个存储单元,你家的地址是你家的指针,假设你叫张三,那“张三家”,就是你家的别名,也就是你家的变量名,你家地址在物业通讯录里的那条记录,就是你家地址的指针,也就是你家指针的指针。

理论上,指针可以有很多级,或是很多层,但实际应用中,别太复杂,不然就分不清了。

编程基础-二进制与存储单元

前言

这是本系列的第一篇,目标受众是0基础的,想学编程的人。

二进制

说起二进制,首先想到的是十进制。平时我们数数,就是用的十进制,为什么叫十进制呢?因为数的过程中,从0开始,每次+1,都会让数增大,但位数不变,都是1个数字,直到9+1=10,这时,1个数字不够用了,用了2个数字,这两个数字组合成一个新的数。这就叫进1。所谓的进1,也就是个数,准确的说,是位数,增加1个。十进制,就是逢十进一。个位进一位到十位,十位进一位到百位。

类比十进制,二进制,就是逢二进一,这时就不能叫个位,十位,百位了,叫第0位,第1位,第2位……。

第0位,逢二进一,那数的时候,0,表示0,1,表示1,10呢?表示的是2,11呢?表示的是3。但是我们也发现了,单独写10,看不出是十进制的十,还是二进制的10,为了区分,就需要用一个标识。计算机里用的是0b前缀表示二进制,而十进制,不用加前缀。

下面是二进制数对0-10的数据的表达。

二进制 十进制
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 8
1001 9
1010 10

为什么用二进制

明明大家都是用的十进制,为什么要发明二进制呢?因为二进制方便计算机计算。为什么计算机方便用二进制计算呢?

二进制的基本数字,就是0,1,而十进制的基本数字是0-9,你能想到什么样的电路可以实现0-9的表达么?在电学里,电信号,一般就是电压、电流、电容、电感、电阻。要想找到不的数字的表达,这些表达就需要分成不同的等级,类似楼梯一样。如果是用电压来表示0-9的数字,按前面分析的,需要找到10个不同的等级来分别表示0-9,而二进制呢,只需要用2个不同的等级来表示0,1,很明显,二进制更简单一些。而十进制要复杂的多。所以,在电子计算机里,二进制得到了广泛的应用。

题外话:在以前,计算机也并不全是二进制的,苏联曾经开发出三进制的计算机,后面应用范围远不及二进制的,也就慢慢消失了。

其他进制

事实上,我们生活中也不只是有十进制,还有十二进制,就是时间,上午1点到12点,下午又是1点到12点。

在计算机领域中,除了二进制、十进制,还有十六进制、八进制。实际用的多的,就是二进制、十六进制和十进制。

数据的存储

可以存数据的东西,有光盘、磁带、硬盘等等,以上这些,存储的原理多少有些不同,但是本质是一样的。它们存的都是0或1,像光盘,是用激光在盘面上烧刻出不同长度的沟,短的是0,长的是1,磁带,是用强磁让磁带一段一段的磁化,留下不同长度的磁条,长的磁条是1,短的磁条是0。磁盘与磁带相似。

也就是说,他们都是存的0或1,用留下的记号的不同来标识。

扯个题外话。0或1这样的数据,本身是不连续的,因为在0和1之间还有无穷多个小数。这些不连续的数据,也被称为离散的数据,那计算机中有连续的数据么?很不幸,没有。计算机中不能直接处理和存储连续的数据。比如一段音乐,如果是用胶片存,胶片存的就是连续的,而CD盘,就不是连续的。那离散的为什么听不出来?同一段音乐,为什么又有胶片存的,又有CD存的,还有MP3文件存的?这些就涉及到了一些模数转换和数模转换了。简写为A/D转换(模数转换),D/A转换(数模转换)。

回到正题。从前面知道,数据计算机中存的是0,1这样的数。但是这些数要想好记,就需要给他们分个段,就像背电话号码,大家都用335或344的方式来分段记忆。同样,为了方便对二进制进行管理,在存储的时候,也对二进制进行了分段。

对于单一1个0或1,叫做一个bit,或比特,翻译过来,就是位。8个bit,就像前面写0-10那样,8个0/1组合,叫做一个字节,byte,为什么是8bit一个字节呢?因为最早计算机是美国搞的,他们把常用的字符编了一个表,用不同的数字表示不同的字符,刚好是127个,也就是7位,7这个数不利于分段,就搞了8位,4位一段。刚好8是2的3次方,8位最多可以表示2的8次方(255)个数。

数据在存储的时候,也是遵循这样的规律,最小的单位是1bit,但是这个单位太小了,就像分是中国货币的最小单位,但是现在很少有人直接说多少分,最少也是多少元。数据在存的时候也是,bit这个单位太小了。比bit大一级的,就是字节,byte,这个用的多一些。也是实际使用中,存储的最小单位。

计算机的数据存储,可以想像成一个大箱子,为了方便收纳,大箱子里又分了很多的区域,每个区域里又分了很多的格子。一个格子里只能存一个字节的数据,这一个字,就是最小的一个存储单元。在硬盘中,这个箱子,就对应着整块硬盘,不同的区域,就是不同的扇形区域,简称扇区,每个扇区下,又包括许多的字节。

数据索引

有过找东西经验的都会发现一个问题。东西是存起来了,但是想找的时候,如果没有提前记录存储位置,找起来就要一个格子一个格子的翻找,效率很低。怎么解决呢?一个很容易想到的方法是提前做一个记录,一条一条的记录着每个格子里存的是什么。这个记录表,不能离开这个箱子,他也要存在箱子里。当你有很多箱子的时候,为了方便找这个记录表,最好的方法是把这个表放在一个固定位置。硬盘里也是这样,在开始的位置,有一个文件记录表,记录表里记录着很个格子里都有什么内容。想要找某个东西的时候,先到这里找到记录表,再在记录表中找有没有想要找的东西。

这里忽略了一个问题,就是格子很多的时候,怎么区分不同的格子呢?聪明如你,给格子编上号就好了。按照一个固定的顺序,把每个格子都编上号码。这样,在找到要找的东西时,就知道了东西所在格子的编号。这像极了门牌号,所以,这个编号,又可以叫它 “地址”。当你开始在记录表中找要找的东西到你找到它的这段时间,在计算机里,叫 寻址

GNOME C规范

以下内容来自GNOME的开发文档,https://developer.gnome.org/documentation/guidelines/programming/coding-style.html

缩进

Linux 内核风格是一个tab缩进8个字符,GNU风格是一个tab 2个字符,这两种都可以,但要统一。

括号

if else,如果只有一条语句,可以不写大括号,如下:

1
2
3
4
5
/* valid */
if (condition)
single_statement ();
else
another_single_statement (arg1);

但也有例外。

第一种,if语句有大括号,else语句也要有大括号。

1
2
3
4
5
6
7
8
9
10
/* valid */
if (condition)
{
foo ();
bar ();
}
else
{
baz ();
}

第二种,单个语句写了多行,也要用大括号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* valid Linux kernel style */
if (condition) {
a_single_statement_with_many_arguments (some_lengthy_argument,
another_lengthy_argument,
and_another_one,
plus_one);
} else
another_single_statement (arg1, arg2);

/* valid GNU style */
if (condition)
{
a_single_statement_with_many_arguments (some_lengthy_argument,
another_lengthy_argument,
and_another_one,
plus_one);
}
else
{
another_single_statement (arg1, arg2);
}

第三种,如果if的条件有多行,也需要用大括号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* valid Linux kernel style */
if (condition1 ||
(condition2 && condition3) ||
condition4 ||
(condition5 && (condition6 || condition7))) {
a_single_statement ();
}

/* valid GNU style */
if (condition1 ||
(condition2 && condition3) ||
condition4 ||
(condition5 && (condition6 || condition7)))
{
a_single_statement ();
}

第四种情况,if嵌套,这时也需要用大括号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* valid Linux kernel style */
if (condition) {
if (another_condition)
single_statement ();
else
another_single_statement ();
}

/* valid GNU style */
if (condition)
{
if (another_condition)
single_statement ();
else
another_single_statement ();
}

函数

函数返回值类型独占一行

1
2
3
4
5
void
my_function (void)
{
// ...
}

函数参数一行一个,参数名称左对齐

1
2
3
4
5
6
7
8
void
my_function (some_type_t type,
another_type_t *a_pointer,
double_ptr_t **double_pointer,
final_type_t another_type)
{
// ...
}

空格

在括号前加空格,不要在括号后加空格

1
2
3
4
5
if (condition)
do_my_things ();

switch (condition) {
}

定义结构体时,使用空行,将不同类型的数据隔开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct _GtkWrapBoxPrivate
{
GtkOrientation orientation;
GtkWrapAllocationMode mode;

GtkWrapBoxSpreading horizontal_spreading;
GtkWrapBoxSpreading vertical_spreading;

guint16 spacing[2];

guint16 minimum_line_children;
guint16 natural_line_children;

GList *children;
};

不要随便删除空行或空格,下面是错误的示范

1
2
/* invalid */
if (condition) foo (); else bar ();

switch

每个case的缩进级别应该相同,而且case的缩进应该与switch的大括号级别一致,一个case结束后,加一个空行,再开始下一个case。

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
/* valid Linux kernel style */
switch (condition) {
case FOO:
do_foo ();
break;

case BAR:
do_bar ();
break;
}

/* valid GNU style */
switch (condition)
{
case FOO:
do_foo ();
break;

case BAR:
do_bar ();
break;

default:
do_default ();
}

头文件

头文件中函数定义要用三列。

1
2
3
return_type          function_name           (type   argument,
type argument,
type argument);

每列最大的宽度要以每列中最大的宽度为准。

1
2
3
4
void        gtk_type_set_property (GtkType     *type,
const char *value,
GError **error);
const char *gtk_type_get_property (GtkType *type);

如果要定义一个公共库,把小的头文件包括到单一的一个公共库头文件中,而不是在应用中导入多个小的头文件。比如,GTK库定义了一些头文件,但是能直接调用的,就是gtk.h,其他的库不能在应用中直接调用。

1
2
3
4
5
6
// The __GTK_H_INSIDE__ symbol is defined in the gtk.h header
// The GTK_COMPILATION symbol is defined only when compiling
// GTK itself
#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
#error "Only <gtk/gtk.h> can be included directly."
#endif

对于库,所有的头文件都要有inclusion guards。下面这串代码演示了如何使用#ifndef和#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
#pragma once
//Time Class definition
//member functions are declared in time.cpp

//prevent multiple inclusions of header
#ifndef TIME_H
#define TIME_H

//time class definition
class time
{
public:
Time();//constructor
void setTime(int, int, int);//set hour, minute and second
void printUniversal() const;//print time in universal-time format
void printStandard() const;//print time in standard-time format

private:
unsigned int hour;//0-23
unsigned int minute;//0-59
unsigned int second;//0-59

};

#endif // !TIME_H

GObject 类

类型声明应该放在文件最开始的位置

1
2
typedef struct _GtkBoxedStruct       GtkBoxedStruct;
typedef struct _GtkMoreBoxedStruct GtkMoreBoxedStruct;

下面的没看懂什么意思……

内存申请

在堆栈中动态申请内存,用g_new(), 公共的结构类型要填充0,或是使用g_new0()来申请内存。

宏Macros

尽量避免用私有宏,返回的时候,记得#undef它们,最好是使用inline函数。公共宏除非是返回一个常值,否则不要使用。

gtk gdb调试时不显示界面的一种情况

问题环境:win10,ucrt64,gtk4

上午的时候在vscode下成功调试了gtk的程序,但是晚上就不行了,能进到gdb,但是不显示窗口,网上查也没查到原因。后面回想了下当前和早上时候的不同,好像就是打开了一个glade的界面,而glade打开的时候,会占用ucrt64的一个控制台。

有了猜想就验证,关闭glade,再调试,一小会,界面就出来了。看来还真的是这个原因。后面又打开glade,再调试,还是不行,调试运行时,关闭glade,一小会儿,窗口就显示出来了。

gtk使用ui文件定义界面运行报错

今天在照着gtk的官方教程做示例的时候,遇到一个问题,就是下面的链接,https://docs.gtk.org/gtk4/getting_started.html

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
#include <gtk/gtk.h>
#include <glib/gstdio.h>

static void
print_hello (GtkWidget *widget,
gpointer data)
{
g_print ("Hello World\n");
}

static void
quit_cb (GtkWindow *window)
{
gtk_window_close (window);
}

static void
activate (GtkApplication *app,
gpointer user_data)
{
/* Construct a GtkBuilder instance and load our UI description */
GtkBuilder *builder = gtk_builder_new ();
gtk_builder_add_from_file (builder, "builder.ui", NULL);

/* Connect signal handlers to the constructed widgets. */
GObject *window = gtk_builder_get_object (builder, "window");
gtk_window_set_application (GTK_WINDOW (window), app);

GObject *button = gtk_builder_get_object (builder, "button1");
g_signal_connect (button, "clicked", G_CALLBACK (print_hello), NULL);

button = gtk_builder_get_object (builder, "button2");
g_signal_connect (button, "clicked", G_CALLBACK (print_hello), NULL);

button = gtk_builder_get_object (builder, "quit");
g_signal_connect_swapped (button, "clicked", G_CALLBACK (quit_cb), window);

gtk_widget_set_visible (GTK_WIDGET (window), TRUE);

/* We do not need the builder any more */
g_object_unref (builder);
}

int
main (int argc,
char *argv[])
{
#ifdef GTK_SRCDIR
g_chdir (GTK_SRCDIR);
#endif

GtkApplication *app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);

int status = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);

return status;
}

官方给的代码,没有说builder.ui这个文件放在哪里,我就放到了和main.c同级的子目录下,结果编译能通过,但是运行时报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(main.exe:53272): Gtk-CRITICAL **: 23:14:02.343: gtk_window_set_application: assertion 'GTK_IS_WINDOW (window)' failed

(main.exe:53272): GLib-GObject-CRITICAL **: 23:14:02.348: invalid (NULL) pointer instance

(main.exe:53272): GLib-GObject-CRITICAL **: 23:14:02.351: g_signal_connect_data: assertion 'G_TYPE_CHECK_INSTANCE (instance)' failed

(main.exe:53272): GLib-GObject-CRITICAL **: 23:14:02.356: invalid (NULL) pointer instance

(main.exe:53272): GLib-GObject-CRITICAL **: 23:14:02.360: g_signal_connect_data: assertion 'G_TYPE_CHECK_INSTANCE (instance)' failed

(main.exe:53272): GLib-GObject-CRITICAL **: 23:14:02.364: invalid (NULL) pointer instance

(main.exe:53272): GLib-GObject-CRITICAL **: 23:14:02.367: g_signal_connect_data: assertion 'G_TYPE_CHECK_INSTANCE (instance)' failed

(main.exe:53272): Gtk-CRITICAL **: 23:14:02.372: gtk_widget_set_visible: assertion 'GTK_IS_WIDGET (widget)' failed

由于刚开始学,也不太会调试,完全没有头绪。但是编译能通过,觉得代码应该是没问题的。就猜,可能是ui文件路径不对。就试着改了两下,最后试出来了,gtk_builder_add_from_file (builder, "builder.ui", NULL); 这句中的builder.ui应该写这个项目中的相对路径,或是绝对路径。我放到了项目目录的src文件夹下,用相对路径写法,应该写成gtk_builder_add_from_file (builder, "src/builder.ui", NULL);,再次make run,就不再报错了。

如果这个以后想换个位置也能运行,这个ui文件应该放到另外的一个目录下,到时再研究怎么发布exe吧。

windows安装gtk4并配置VSCode进行编程

下载MSYS2

gtk的网站上是这样写的

There are various methods to install GTK on Windows development machines.

MSYS2

This method is based on the packages provided by MSYS2, which provides a UNIX-like environment for Windows. Both of these repositories also provide packages for a large number of other useful open source libraries.

gvsbuild

This method provides scripts to build the GTK stack from source and outputs libraries and tools that can be consumed by Visual Studio or Meson based projects.

意思是有两种方法在windows平台上安装gtk,一种是用MSYS2,一种是用gvsbuild。我选的是MSYS2这个路径。

https://www.msys2.org/ 从这个网址下载,一打开就可以看到下载链接,很显眼。跟着指导步骤一步一步做。
msys2下载

需要注意的是,按照这个网站上的安装方法,会安上 UCRT64 环境,这个环境是基于MinGW64的,很多教程里边,环境变量,include路径基本都是用的MinGW的路径。

安装GTK4和依赖

在第一步完成时,就打开了一个命令行窗口,如果不小心关了,可以从开始菜单找到MSYS2 UCRT64,点它,启动。运行pacman -S mingw-w64-ucrt-x86_64-gtk4,如果想安装gtk3,则运行pacman -S mingw-w64-ucrt-x86_64-gtk3

安装工具链(可选)

如果要在C、C++等语言下使用GTK,需要安装编译器,比如GCC和它的工具链。pacman -S mingw-w64-ucrt-x86_64-toolchain base-devel,如果是使用Python,需要安装Python包,pacman -S mingw-w64-ucrt-x86_64-python-gobject

配置VSCode

由于VSCode免费,支持多种语言,所以计算机上安了VSCode,不想再安其他的IDE,就配一下VSCode吧。结果遇到了很多坑。

设置环境变量

在系统环境变量的PATH中,填加MSYS2中的msys64\ucrt64\bin,当然,主要还是看哪个目录的bin文件夹下有内容。

设置完环境变量,打开cmd,输入gcc --version,可以看到回显数据。

安装VSCode插件

一个项目管理的Easy C++ project,一个是C/C++代码提示的Microsoft C/C++ 扩展

创建C++项目

vscode打开新建的文件夹,按下键盘F1输入easy cpp,选择Create new C++ proejct,然后选择[G++/GDB] Linux,Windows系统也是选这个,因为只有这个才是用mingw的。

获取include path

打开MSYS2 UCRT64 在命令窗口输入pkg-config --cflags gtk4 | sed 's/ /\n/g' | sed '1,3d;s/-I\(.*\)/\1/g' | sort | uniq,将输出的结果复制。注意,复制的时候,有几项是短横线-打头的,不是文件路径的项,不要复制这些。

设置include path

在vscode按下F1,选择C/C++:编辑配置(UI),这将自动生成c_cpp_properties.json文件并且进入图形界面进行修改。将前面复制的路径,贴到包含路径处。注意,图片只是示例,不是真实值。这里的路径,不太全,需要手动再填加两个,一个是include/gtk-4.0,一个是include/pango-1.0。如果不加这两个,VSCode的代码检查总是会提示有文件找不到,其实没啥事。

设置includePath

修改Makefile

下面是我用的Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CXX		  := g++
CXX_FLAGS := `pkg-config --cflags gtk4`

BIN := bin
SRC := src
INCLUDE := include
LIB := lib

LIBRARIES := `pkg-config --libs gtk4`
EXECUTABLE := main


all: $(BIN)/$(EXECUTABLE)

run: clean all
clear
./$(BIN)/$(EXECUTABLE)

$(BIN)/$(EXECUTABLE): $(SRC)/*.cpp
$(CXX) $(CXX_FLAGS) -I$(INCLUDE) -L$(LIB) $^ -o $@ $(LIBRARIES)

clean:
-rm $(BIN)/*

launch.json和task.json

做出上面的修改后,编译是可以了,但是要手动打命令,如果要点下面的build & run,则需要修改task.json,第一次运行时,会在右下角提示生成task.json。我改成下面这样了

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
{
"version": "2.0.0",
"tasks": [
{
"label": "Build C++ project",
"type": "shell",
"group": "build",
"command": "make"
},
{
"label": "Build & run C++ project",
"type": "shell",
"group": {
"kind": "test",
"isDefault": true
},
"command": "make",
"args": [
"run"
]
},
{
"type": "cppbuild",
"label": "C/C++: g++.exe 生成活动文件",
"command": "C:/msys64/ucrt64/bin/g++.exe",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${fileDirname}\\${fileBasenameNoExtension}.exe"
],
"options": {
"cwd": "C:/msys64/ucrt64/bin"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "调试器生成的任务。"
}
]
}

调试的话,需要配置launch.json。

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
{
"version": "0.2.0",
"configurations": [
{
"name": "C++ Debug (gdb)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/main",
"preLaunchTask": "Build C++ project",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true,
"MIMode": "gdb",
"miDebuggerPath": "gdb.exe",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}

线规格,端子规格,压线钳规格

线规格

线规格现在主要说美标的,以AWG标识,数字越大,线越细。

线规格.png

图上展示了不同规格的线的直径、截面积、导流能力。

端子规格

端子形式有很多,但与线相连接的部分就三种形式。

柱状端子

把线穿进圆柱里,把圆柱夹线,把线固定,这种一般线不是很粗,线套上这个端子后,可以很方便的接到种子端子排上。

柱状端子

类似杜邦头端子

压线时两片压线芯,两片压线皮,需要用专用钳子压端子。

类杜邦头端子

大圆柱鼻状端子

有较大的线鼻,一般是U形口,或是扁口的端子上用,压的时候与第二种类似,但是一般是压中间一处,而不是两处。

大线鼻

压线钳规格

不论是何种端子,都有适应的线径,不是一个型号的端子可以压任意规格的线。而不同形式压接方式,可以选不同形式的钳子,但是钳子的规格,一般是以线径来说的。比如一个端子用的16AWG的线,那钳子应要用适配16AWG的钳子。由于线规格很多,所以钳子一般一个头上有多个槽位,可以压不同的线径,线径跨度再大点,还要换其它的钳子。大钳子压小线,压不紧,小钳子压大线,容易变形,甚至断线。