2024年4月

Avalonia是一个强大的跨平台UI框架,允许开发者构建丰富的桌面应用程序。

它提供了众多UI组件、灵活的布局系统、可定制的样式以及事件处理机制。

在这篇博客中,我们将详细解析Avalonia的UI组件、UI组件的生命周期、布局、样式和事件处理。

一、UI组件

Avalonia提供了丰富的UI组件,包括按钮(Button)、文本框(TextBox)、列表框(ListBox)等。这些组件可以通过XAML或C#代码进行创建和配置。

示例代码:

在XAML中定义一个按钮:

<Windowxmlns="https://github.com/avaloniaui"Title="Avalonia UI Components">  
    <StackPanel>  
        <ButtonContent="Click Me" />  
    </StackPanel>  
</Window>

在C#代码中创建一个按钮:

Button button = new Button { Content = "Click Me"};this.Content = button; //假设this是一个Window实例

二、UI组件的生命周期

UI组件在Avalonia中也有着明确的生命周期。它们会经历创建、附加到视觉树、更新、从视觉树分离以及销毁等阶段。

示例代码:

在组件创建时注册事件处理程序:

Button button = new Button { Content = "Click Me"};  
button.AttachedToVisualTree
+= (sender, e) =>{//组件已附加到视觉树,可以进行一些初始化操作 Console.WriteLine("Button attached to visual tree.");
};

在组件销毁时清理资源:

button.DetachedFromVisualTree += (sender, e) =>{//组件已从视觉树分离,可以进行清理操作
    Console.WriteLine("Button detached from visual tree.");//清理资源...
};

三、布局

Avalonia提供了强大的布局系统,允许开发者以灵活的方式组织UI组件。常见的布局容器包括StackPanel、Grid和DockPanel等。

示例代码:

使用StackPanel进行垂直布局:

<Windowxmlns="https://github.com/avaloniaui"Title="Avalonia Layout">  
    <StackPanel>  
        <ButtonContent="Button 1" />  
        <ButtonContent="Button 2" />  
        <ButtonContent="Button 3" />  
    </StackPanel>  
</Window>

使用Grid布局容器

<GridRowDefinitions="Auto,Auto"ColumnDefinitions="1*, 1*">
    <ButtonGrid.Row="0"Grid.Column="0"Content="Button 1" />
    <ButtonGrid.Row="0"Grid.Column="1"Content="Button 2" />
    <TextBoxGrid.Row="1"Grid.Column="0"Grid.ColumnSpan="2"Text="Cross-column TextBox" />
</Grid>

在上面的示例中,我们使用Grid布局容器来组织按钮和文本框。通过设置RowDefinitions和ColumnDefinitions属性,我们定义了网格的行和列。然后,通过Grid.Row、Grid.Column和Grid.ColumnSpan等附加属性,我们将组件放置在网格的特定位置。

四、样式

Avalonia支持通过样式来定义UI组件的外观。样式可以应用于单个组件,也可以应用于整个应用程序。

示例代码:

在XAML中定义全局样式:

<Windowxmlns="https://github.com/avaloniaui"Title="Avalonia Styles">  
    <Window.Styles>  
        <StyleSelector="Button">  
            <SetterProperty="Background"Value="LightBlue"/>  
        </Style>  
    </Window.Styles>  
    <StackPanel>  
        <ButtonContent="Styled Button" />  
    </StackPanel>  
</Window>

五、事件处理

Avalonia支持事件处理机制,允许开发者响应用户的输入和操作。例如,可以监听按钮的点击事件,或者在文本框内容发生变化时执行某些操作。

示例代码:

监听按钮的点击事件:

Button button = new Button { Content = "Click Me"};  
button.Click
+= (sender, e) =>{//处理按钮点击事件 Console.WriteLine("Button clicked!");
};

监听文本框的文本变化事件:

TextBox textBox = newTextBox();  
textBox.TextChanged
+= (sender, e) =>{//处理文本框文本变化事件 Console.WriteLine("Text changed:" +textBox.Text);
};

总结:

通过本博客的解析,我们了解了Avalonia的UI组件、UI组件的生命周期、布局、样式和事件处理等关键概念,并给出了相应的示例代码。

Avalonia作为一个跨平台的UI框架,提供了丰富的功能和灵活的机制,使得开发者能够轻松地构建出美观且功能强大的桌面应用程序。

gRPC入门学习之旅(一)

通过之前的文章,我们已经创建了gRPC的服务端应用程序,那么应该如何来使用这个服务端应用程序呢,接下来介绍如何通过客户端来使用这个服务端应用程序。

3、创建gRPC客户端

3.1、创建gRPC的控制台客户端项目

1. 在Visual Studio 2022菜单栏上选择“文件—》新建—》项目”。

或者在Visual Studio 2022的解决方案资源管理器中,使用鼠标右键单击“解决方案‘Demo.GrpcService’”,在弹出的快捷菜单中选择“添加—》新建项目”。如下图。

2. Visual Studio 2022弹出的“创建新项目”的对话框中做如下选择。如下图。

  • 在最左边的下拉框中,选择 “C# ,如下图中1处
  • 在中间的下拉框中,选择 “所有平台”,如下图2处。
  • 在最右边的下拉框中,选择“控制台”,如下图3处。
  • 在下图中4处,选择“控制台应用”模板,点击“下一步”按钮。

4.在弹出的“配置新项目”的对话框中,在“项目名称”输入框中,输入“Demo.Grpc.Cmd”。然后使用鼠标点击“下一步”按钮。

5. 在弹出的“其他信息”的对话框,在“框架”下拉框中,选择“NET 7.0(标准期限支持)”。其他值选择默认值即可。然后使用鼠标点击“创建”按钮。项目创建成功。

6.在解决方案资源管理器中——>在项目Demo.Grpc.Cmd中的依赖项上鼠标右键单击——>弹出一个菜单,选中“管理NuGet程序包”,如下图。

7. 在打开的NuGet包管理界面的中选择“浏览”标签页,在搜索框中输入“google”,找到最新版本google.protobuf,点击安装。如下图。

8.  Visual Studio 2022 开始安装Google.Protobuf,会弹出安装确认界面,点击“OK”按钮。

9. 在打开的NuGet包管理界面的中“浏览”标签页的搜索框中输入“grpc”,然后依次安装以下三个包。

Grpc.Net.Client

Grpc.Tools

Grpc.Net.ClientFactory

10. 在以上四个包全部安装完成之后,NuGet包管理界面的中选择“已安装”标签页,会看到刚才安装的四个包,如下图。

3.2、添加Proto协议文件引用

1. 在 Visual Studio 2022 的“解决方案资源管理器”中,使用鼠标右键单击“Demo.Grpc.Cmd”,在弹出的快捷菜单中选择“添加—》服务引用或是连接的服务”。“服务引用”和“连接的服务”均在同一个UI 下进行管理。 如下图。

2.在出现的工具窗口中,选择“连接的服务”部分,然后在“服务引用”部分中选择“添加服务引用”,选择“gRPC”并点击“下一步”。如下图。

3.在“添加新的gRPC服务引用”弹出界面中,选择“文件”,点击“浏览”按钮,在弹出对话框中找到 Demo.GrpcSERVICE 项目中的 protos 文件夹中的UserInfo.proto,在“选择要生成的类的类型”下保留“客户端”,然后选择“完成”。如下图。

4.在添加完UserInfo.proto文件之后,界面返回到“连接的服务”界面,会看到服务引用中有我们刚才添加的引用文件。如下图。

5. 使用 Visual Studio 2022的“添加连接的服务”功能时,UserInfo.proto 文件将作为链接文件而不是副本文件添加到类库项目中,因此对服务项目中文件的更改将自动应用到客户端项目中。 csproj 文件中的 <Protobuf> 元素如下所示:

<ItemGroup>

<ProtobufInclude="..\Demo.GrpcService\Protos\UserInfo.proto"GrpcServices="Client">
<Link>Protos\UserInfo.proto</Link>
</Protobuf>

</ItemGroup>

1. re 的match和search区别?
re.match 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match()就返回none。
re.search 扫描整个字符串并返回第一个匹配成功的值。
2. 什么是正则的贪婪匹配?
匹配一个字符串没有节制,能匹配多少就去匹配多少,直到没有匹配的为止。
3. 求结果:
a. [i % 2 for i in range(10)]
print([i % 2 for i in range(10)])	# [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
print([i for i in range(10)])	# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print([10 % 2])		# [0]
# % 是个运算符。
b. (i % 2 for i in range(10))
print((i % 2 for i in range(10)))
# <generator object <genexpr> at 0x00000233D5D45EB0> 生成器
# 在py中,有一种自定义迭代器的方式,称为生成器(Generator)。
# 定义生成器的两种方式:
# 1. 创建一个generator,只要把一个列表生成式的[]改成(),就创建了一个generator:generator保存的是算法,每次调用next(),就计算出下一个元素的值,直到计算到最后一个元素。没有更多的元素时,抛出StopIteration的错误。
# 2. 定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通的函数,而是一个generator。
4. 求结果
a. 1 or 2	# 1
b. 1 and 2	# 2
c. 1 < (2 == 2)		# False
d. 1 < 2 == 2	# True
5. 如何实现 "1,2,3" 变成 ['1', '2', '3'], 反过来呢?
list("1, 2, 3".split(','))
[int(x) for x in ['1', '2', '3']]
6. 一行代码实现删除列表中重复的值?
list(set([1, 2, 3, 4, 1, 2, 5, 6, 4]))
7. 如何在函数中设置一个全局变量?

py中的global语句是被用来声明全局变量的。

x = 2
def func():
	global x
	x = 1
	return x
func()
print(x)	# 1
8. logging 模块的作用以及应用场景?
logging 模块定义的函数和类为应用程序和库的开发实现了一个灵活的事件日志系统。
作用:可以了解程序运行情况,是否正常,在程序的出现故障快速定位出错地方及故障分析。
9. 请用代码简答实现 stack?
  • Stack() 创建一个新的空栈
  • push(item) 添加一个新的元素item到栈顶
  • pop() 弹出栈顶元素
  • peek() 返回栈顶元素
  • is_empty() 判断栈是否为空
  • size() 返回栈的元素个数
# 实现一个栈stack,后进先出
class Stack:
    def __init__(self):
        self.items = []

    def is_empty(self):
        # 判断是否为空
        return self.items == []

    def push(self, item):
        # 加入元素
        self.items.append(item)

    def pop(self):
        # 弹出元素
        return self.items.pop()

    def peek(self):
        # 返回栈顶元素
        return self.items[len(self.items) - 1]

    def size(self):
        # 返回栈的大小
        return len(self.items)

if __name__ == '__main__':
    stack = Stack()
    stack.push("H")
    stack.push("E")
    stack.push("L")
    print(stack.size())     # 3
    print(stack.peek())     # L
    print(stack.pop())      # L
    print(stack.pop())      # E
    print(stack.pop())      # H
10. 常用字符串格式化有哪几种?
  • 占位符 %
    %d 表示那个位置是整数;%f 表示浮点数;%s表示字符串。
print('Hello, %s' % 'Python')   # Hello, Python
print('Hello, %d %s %2f' % (666, 'Python', 9.99))
#   Hello, 666 Python 9.990000
  • format
print('{k} is {v}'.format(k = 'python', v = 'easy'))    # python is easy
print('{0} is {1}'.format('python', 'easy'))    # python is easy
11. 简述生成器、迭代器、可迭代对象、装饰器?
  1. 生成器:
    包括含有yield关键字,生成器也是迭代器,调动next把函数变成迭代器。

  2. 迭代器:
    含有
    __iter__

    __next__
    方法(包含
    __iter__
    方法的可迭代对象就是迭代器)

  3. 可迭代对象:
    一个类内部实现
    __iter__
    方法且返回一个迭代器。

  4. 装饰器:
    能够在不修改原函数代码的基础上,在执行前后进行定制操作,闭包函数的一种应用。
    场景:

  • flask 路由系统
  • flask before_request
  • csrf
  • django 内置认证
  • django 缓存
import functools

def wrapper(func):
    @functools.wraps(func)      # 不改变原函数属性
    def inner(*args, **kwargs):
        # 执行函数前
        return func(*args, **kwargs)
        # 执行函数后
    return inner
# 1. 执行wrapper函数,并将被装饰的函数当做参数。  wrapper(index)
# 2. 将第一步的返回值,重新赋值给新index = wrapper(老index)
@wrapper    # index=wrapper(index)
def index(x):
    return x+100

调用装饰器其实是一个闭包函数,为其他函数添加附加功能,不修改被修改的源代码和不修改被修饰的方式,装饰器的返回值也是一个函数对象。
比如:插入日志、性能测试、事物处理、缓存、权限验证等,有了装饰器,就可以抽出大量与函数功能本身无关的雷同代码并继续重用。

12. def func(a, b=[]) 这种写法有什么坑?
def func(a, b=[]):
    b.append(1)
    print(a, b)

func(a=2)   # 2 [1]
func(2)     # 2 [1, 1]
func(2)     # 2 [1, 1, 1]
想每次执行只输出[1],默认参数应该设置为None
13. 列举常见的内置函数
  • abs() 返回数字的绝对值
  • map() 根据函数对指定序列做映射
    map() 函数接受两个参数,一个是函数,一个是可迭代对象,map将传入的函数依次作用到序列的每个元素,并把结果作为新的list返回。

返回值:
py2:返回列表
py3:返回迭代器

例子1:

def mul(x):
    return x*x
n = [1, 2, 3, 4, 5]
res = list(map(mul, n))
print(res)  # [1, 4, 9, 16, 25]

例子2:

ret = map(abs, [-1, -5, 6, -7])
print(list(ret))    # [1, 5, 6, 7]
  • filter
    filter()函数接收一个函数 f(函数)和一个list(可迭代对象),这个函数 f的作用是对每个元素进行判断,返回True或False,filter()根据判断结果自动过滤掉不符合条件的元素,返回由符合条件元素组成的新list。
def is_odd(x):
    return x % 2 == 1
v = list(filter(is_odd, [1, 4, 6, 7, 9, 12, 17]))
print(v)    # [1, 7, 9, 17]

map 与 filter 总结

参数:都是一个函数名 + 可迭代对象
返回值:都是返回可迭代对象
区别:filter是做筛选的,结果还是原来就在可迭代对象中的项。map是对可迭代对象中每一项做操作的,结果不一定是原来就在可迭代对象中的项
  • isinstance/type
    isinstance()函数来判断一个对象是否是一个已知的类型,类似type()。
    isinstance() 与 type() 区别:
    type() 不会认为子类是一种父类类型,不考虑继承关系。
    isinstance() 会认为子类类型是一种父类类型,考虑继承关系。
    如果要判断两个类型是否相同推荐使用 isinstance()。
a = 2
print(isinstance(a, int))   # True
print(isinstance(a, str))   # False
class A:
    pass
class B(A):
    pass
print("isinstance", isinstance(A(), A))     # isinstance True
print("type",type(A()) == A)     # type True
print("isinstance", isinstance(B(), A))     # isinstance True
print("type",type(B()) == A)     # type False
  • 拉链函数
    zip 拉链函数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表迭代器。如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同。
print(list(zip([0, 1, 3], [5, 6, 7], ['a', 'b'])))  # [(0, 5, 'a'), (1, 6, 'b')]

zip() 函数用于将可迭代对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。

>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> c = [4, 5, 6, 7, 8]
>>> zipped = zip(a, b)	# 打包为元组的列表 [(1, 4), (2, 5), (3, 6)]
>>> zip(a, c)	# 元素个数与最短的列表一致 [(1, 4), (2, 5), (3, 6)]
>>> zip(*zipped)	# 与zip相反,可理解为解压,返回二维矩阵式	[(1, 2, 3), (4, 5, 6)]
  • reduce
    reduce() 函数会对参数序列中元素进行累积。函数将一个数据集合(链表、元组等)中的所有数据进行下列操作
    注意:py3已经将reduce()函数从全局名字空间里移除了,它现在被放置在functools模块里,如果想要使用它,则需要用过引用引入functools模块来调用reduce()函数。
from functools import reduce
def add(x, y):
    return x + y
print(reduce(add, [1, 2, 3, 4, 5]))     # 15
print(reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]))    # 15
print(reduce(add, range(1, 101)))   # 5050
14. filter、map、reduce 的作用?

内置函数:map、reduce、filter 的用法和区别

  • map:根据函数对指定序列做映射
map
参数:接收两个参数一个是函数,一个是序列(可迭代对象)
返回值:py2返回列表,py3返回迭代器
例子:
abs() 函数返回数字的绝对值
新的内容的个数等于原内容的个数
ret = map(abs, [-1, -5, 6, -7])
print(list(ret))	# [1, 5, 6, 7]
  • filter:过滤函数 新的内容少于等于原内容的时候。才能使用filter,filter()函数用于过滤序列,过滤不符合条件的元素,返回由符合条件元素组成的新列表。

参数:
function 函数
iterable 可迭代对象

返回值:返回列表

# 筛选大于 10 的数
def is_odd(x):
    if x > 10:
        return True
ret = filter(is_odd, [1, 4, 5, 7, 8, 9, 76])    # 为迭代器
print(list(ret))    # 76
  • reduce:对于序列内所有元素进行累计操作
    reduce() 函数会对参数序列中元素进行累积,函数将一个数据集合(链表、元组等)中的所有数据进行下列操作。
from functools import reduce
def add(x, y):
    return x + y
print(reduce(add, [1, 2, 3, 4, 5]))     # 15
print(reduce(lambda x, y: x + y, [1, 2, 3, 4, 5]))  # 15
print(reduce(add, range(1, 101)))   # 5050
15. 用py实现一个二分查找的函数?

二分查找算法:简单地说,就是将一个列表先排序好,比如按照从小到大的顺序排列,当给定一个数据,比如3,查找3在列表中的位置时,可以先找到列表中间的数li[middle]和3进行比较,当它比3小时,那么3一定是在列表的右边,反之则3在列表的左边。比如它比3小,则下次就可以只比较[middle+1, end]的数,继续使用二分法,将它一分为二,直到找到3这个数返回或者列表全部遍历完成(3不在列表中)
优点:效率高,时间复杂度为O(logN);
缺点:数据要是有序的,顺序存储。

li = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def search(someone, li):
    l = -1
    h = len(li)
    while l + 1 != h:
        m = int((l + h) / 2)
        if li[m] < someone:
            l = m
        else:
            h = m
    p = h
    if p >= len(li) or li[p] != someone:
        print("元素不存在")
    else:
        str = "元素索引为%d" % p
        print(str)
search(3, li)   # 元素索引为2
16. 谈谈你对闭包的理解?
def foo():
    m = 3
    n = 5
    def bar():
        a = 4
        return m+n+a
    return bar
bar = foo()
print(bar())   # 12

说明:bar在foo函数的代码块中定义。我们称bar是foo的内部函数。在bar的局部作用域中可以直接访问foo局部作用域中定义的m、n变量。简单地说,这种内部函数可以使用外部函数变量的行为,就叫闭包。
闭包的意义与应用

17. 如何生成一个随机数?
import random
# 生成一个0-1的随机浮点数:0 <= n < 1.0
print(random.random())
# 生成一个指定范围内的整数
print(random.randint(1, 10))
18. 如何使用py删除一个文件?
import os
file = r'E:/test.txt'
if os.path.exists(file):
    os.remove(file)
    print('delete success')
else:
    print('no such file: %s' % file)
19. 谈谈你对面向对象的理解(三大特性及解释)?

面向对象是一种编程思想,以类的眼光来看待事物的一种方式。将共有的属性和方法的事物封装到同一个类下面。
封装:将共同的属性和方法封装到同一个类下面

  • 第一层面:创建类和对象会分别创建二者的名称空间,我们只能用类名. 或者obj. 的方式去访问里面的名字,这本身就是一种封装
  • 第二层面:类中把某些属性和方法隐藏起来(或者说定义成私有的),只在类的内部使用、外部无法访问,或者留下少量接口(函数)供外部访问。

继承:将多个类的共同属性和方法封装到一个父类下面,然后再用这些类来继承这个类的属性和方法

多态:python天生是支持多态的。指的是基类的同一个方法在不同的派生类中有着不同的功能。

20.面向对象中深度优先和广度优先是什么?

Python的类可以继承多个类,如果继承了多个类,那么其寻找方法的方式有两种:
当类是经典类时,多继承情况下,会按照深度优先方式查找。 py3
当类是新式类时,多继承情况下,会按照广度优先方式查找。 py2
简单点说就是:经典类是纵向查找,新式类是横向查找。
经典类和新式类的区别就是,在声明类的时候,新式类需要加上object关键字。在py3中默认全是新式类。

使用
konva
实现一个设计器交互,首先考虑实现设计器的画布。

一个基本的画布:

【展示】网格、比例尺

【交互】拖拽、缩放

“拖拽”是无尽的,“缩放”是基于鼠标焦点的。

最终效果(
示例地址
):

image

image

image

基本思路:

设计区域 HTML 由两个节点构成,内层挂载一个 Konva.stage 作为画布的开始。

<template>
  <div class="page">
    <header></header>
    <section>
      <header></header>
      <section ref="boardElement">
        <div ref="stageElement"></div>
      </section>
      <footer></footer>
    </section>
    <footer></footer>
  </div>
</template>

image

Konva.stage 暂时先设计3个 Konva.Layer,分别用于绘制背景、所有素材、比例尺。

image

通过 ResizeObserver 使 Konva.stage 的大小与外层 boardElement 保持一致。

为了显示“比例尺” Konva.stage 默认会偏移一些距离,这里定义“比例尺”尺寸为 40px。

    this.stage = new Konva.Stage({
      container: stageEle,
      x: this.rulerSize,
      y: this.rulerSize,
      width: config.width,
      height: config.height
    })

关于“网格背景”,是按照当前设计区域大小、缩放大小、偏移量,计算横向、纵向分别需要绘制多少条 Konva.Line(横向、纵向分别多加1条),同时根据 Konva.stage 的 x,y 进行偏移,用有限的 Konva.Line 模拟无限的网格画布。

      // 格子大小
      const cellSize = this.option.size
      //
      const width = this.stage.width()
      const height = this.stage.height()
      const scaleX = this.stage.scaleX()
      const scaleY = this.stage.scaleY()
      const stageX = this.stage.x()
      const stageY = this.stage.y()

      // 列数
      const lenX = Math.ceil(width / scaleX / cellSize)
      // 行数
      const lenY = Math.ceil(height / scaleY / cellSize)

      const startX = -Math.ceil(stageX / scaleX / cellSize)
      const startY = -Math.ceil(stageY / scaleY / cellSize)

      const group = new Konva.Group()

      group.add(
        new Konva.Rect({
          name: this.constructor.name,
          x: 0,
          y: 0,
          width: width,
          height: height,
          stroke: 'rgba(255,0,0,0.1)',
          strokeWidth: 2 / scaleY,
          listening: false,
          dash: [4, 4]
        })
      )

      // 竖线
      for (let x = startX; x < lenX + startX + 1; x++) {
        group.add(
          new Konva.Line({
            name: this.constructor.name,
            points: _.flatten([
              [cellSize * x, -stageY / scaleY],
              [cellSize * x, (height - stageY) / scaleY]
            ]),
            stroke: '#ddd',
            strokeWidth: 1 / scaleY,
            listening: false
          })
        )
      }

      // 横线
      for (let y = startY; y < lenY + startY + 1; y++) {
        group.add(
          new Konva.Line({
            name: this.constructor.name,
            points: _.flatten([
              [-stageX / scaleX, cellSize * y],
              [(width - stageX) / scaleX, cellSize * y]
            ]),
            stroke: '#ddd',
            strokeWidth: 1 / scaleX,
            listening: false
          })
        )
      }

      this.group.add(group)

关于“比例尺”,与“网格背景”思路差不多,在绘制“刻度”和“数值”的时候相对麻烦一些,例如绘制“数值”的时候,需要动态判断应该使用多大的字体。

              let fontSize = fontSizeMax

              const text = new Konva.Text({
                name: this.constructor.name,
                y: this.option.size / scaleY / 2 - fontSize / scaleY,
                text: (x * cellSize).toString(),
                fontSize: fontSize / scaleY,
                fill: '#999',
                align: 'center',
                verticalAlign: 'bottom',
                lineHeight: 1.6
              })

              while (text.width() / scaleY > (cellSize / scaleY) * 4.6) {
                fontSize -= 1
                text.fontSize(fontSize / scaleY)
                text.y(this.option.size / scaleY / 2 - fontSize / scaleY)
              }
              text.x(nx - text.width() / 2)

关于“拖拽”,这里设计的是通过鼠标右键拖拽画布,通过记录 mousedown 时 Konva.stage 起始位置、鼠标位置,mousemove 时将鼠标位置偏移与Konva.stage 起始位置计算最新的 Konva.stage 的位置即可。

      mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
        if (e.evt.button === Types.MouseButton.右键) {
          // 鼠标右键
          this.mousedownRight = true

          this.mousedownPosition = { x: this.render.stage.x(), y: this.render.stage.y() }
          const pos = this.render.stage.getPointerPosition()
          if (pos) {
            this.mousedownPointerPosition = { x: pos.x, y: pos.y }
          }

          document.body.style.cursor = 'pointer'
        }
      },
      mouseup: () => {
        this.mousedownRight = false

        document.body.style.cursor = 'default'
      },
      mousemove: () => {
        if (this.mousedownRight) {
          // 鼠标右键拖动
          const pos = this.render.stage.getPointerPosition()
          if (pos) {
            const offsetX = pos.x - this.mousedownPointerPosition.x
            const offsetY = pos.y - this.mousedownPointerPosition.y
            this.render.stage.position({
              x: this.mousedownPosition.x + offsetX,
              y: this.mousedownPosition.y + offsetY
            })

            // 更新背景
            this.render.draws[Draws.BgDraw.name].draw()
            // 更新比例尺
            this.render.draws[Draws.RulerDraw.name].draw()
          }
        }
      }

关于“缩放”,可以参考 konva 官网的
缩放示例
,思路是差不多的,只是根据实际情况调整了逻辑。

接下来,计划增加下面功能:

  • 坐标参考线
  • 从左侧图片素材拖入节点
  • 鼠标、键盘移动节点
  • 鼠标、键盘单选、多选节点
  • 键盘复制、粘贴
  • 节点层次单个、批量调整
  • 等等。。。

如果 github Star 能超过 20 个,将很快更新下一篇章。

源码
在这
,望多多支持

大家好,我是
知微

学习过单片机的小伙伴对GPIO肯定不陌生,GPIO (general purpose input output)是通用输入输出端口的简称,通俗来讲就是单片机上的引脚。

在STM32中,GPIO的工作模式被细分为8种,对于初学者来讲,要理解它们可太难了!

诶诶诶,给个机会,先别急着退出哈!

这不是有我在呢,跟着这篇文章学习,保证你几分钟时间就能轻松掌握这8种工作模式。

那么,好戏开始咯!

输入输出

首先,我们先要知道一个概念,GPIO的输入输出都是相对于MCU(单片机)来说的。

  • MCU给引脚信号,称之为输出
  • MCU接收引脚给过来的信号,则叫做输入。

知道这个之后,我们就可以进行下一步了,先从输出说起。

四种输出模式

1、 推挽输出

我第一次听到推挽这个词的时候,一脸懵逼,啥玩意儿啊!其实看英文反而好理解,
push-pull
,也就是推拉的意思。

这个叫做推

这个叫做挽

  • 推挽输出模式下,GPIO可以输出高电平,也可以输出低电平。


  • 输出高电平时,P-MOS导通
    ,电流按下图箭头所示流出去,称之为

    ,把电流推出去。


  • 输出低电平时,N-MOS导通
    ,电流按下图箭头所示流进来,称之为

    ,把电流挽回来。

应用场景
:适用于通用的数字输出场景,如点亮LED灯

2、开漏输出

这又是一个不好理解的词,开漏,是不是什么东西开了,然后漏出来了?

其实不是这样的,

是开路的意思。开路表示电路中存在一个断链,电流无法从一个点流到另一个点。

那么肯定有小伙伴会有疑问,开路和断路有啥区别?

这里简单说明一下:

  • 开路表示电路中不存在电流流动;

  • 断路表示电路中某一部分不通过电流流动,但是电路中仍然存在其他电流流动的路径

好了,话题不扯远了,继续说开漏中的漏。

我们知道,MOS管的三个极分别是栅极(G)、源极(S)和漏极(D)。这里的

就是MOS三个极中的漏极。

  • 开漏输出模式下,GPIO可以输出低电平,也可以输出高阻态。
    在此模式下,P-MOS始终处于关断状态

  • 当输出控制器将P-MOS关断、N-MOS导通时,此时
    输出接VSS,输出低电平

  • 当输出控制器将P-MOS关断、N-MOS关断时,相当于什么都没接,此时
    输出浮空,相对于其它点的电阻无穷大,呈现高阻态
    ,可以理解为开路

应用场景
:适用于多个设备共享同一信号线,如I2C通信协议

3、复用推挽输出

  • 和推挽输出同理,只不过此时的输出控制器由片上外设控制

应用场景
:允许GPIO引脚用于微控制器的特定功能,如SPI、I2C、USART等接口,同时保持推挽输出的特性

4、复用开漏输出

  • 和开漏输出同理,只不过此时的输出控制器由片上外设控制

应用场景
:适用于复用功能接口,且需要多设备共享通讯总线(如I2C)的场景

四种输入模式

1、上拉输入

你可以把输入驱动器框中,跟VDD和VSS连接的电阻,想象成两个弹簧。

当VDD的开关闭合时,上拉电阻接通VDD,此时弹簧向上拉。

可以读取I/O引脚状态,默认为高电平。

应用场景
:常用于矩阵键盘或按钮输入

2、下拉输入

当VSS的开关闭合时,下拉电阻接通VSS,此时弹簧向下拉。

可以读取I/O引脚状态,默认为低电平。

应用场景
:如按钮开关连接到地时的检测

3、浮空输入

当VDD和VSS的开关都断开时,此时弹簧既不向上拉,也不向下拉,处于一种悬空的状态。

浮空输入状态下,读取该端口的电平是不确定的。

应用场景
:常用于接收来自开关、键盘或其他数字接口的信号

4、模拟输入

从图示可以看到,之前的3种模式,输入的信号都经过了TTL施密特触发器,把缓慢变化的模拟信号转换成阶段变化的数字信号。而这种模式,信号没有经过施密特触发器,直接接到片上外设。

相较于其他输入模式只能读取到逻辑高/低电平(数字量),该模式能读取到细微变化的值(模拟量)。

通俗来讲就是,别的模式只能读取0和1,而模拟输入可以读取到0-1的变化区间。

主要应用
:读取来自传感器(如温度传感器、电位计)的模拟信号

好了,STM32的8种GPIO端口模式的介绍到这里就结束了,看完之后是不是对这些概念清晰多了。