2024年4月

这是一个挺有意思的面试题,挺简单的,不知道大家平时在重置密码的时候有没有想过这个问题。回答这个问题其实就一句话:因为服务端也不知道你的原密码是什么。如果知道的话,那就是严重的安全风险问题了。

重置帐号密码

我们这里来简单分析一下。

做过开发的应该都知道,服务端在保存密码到数据库的时候,绝对不能直接明文存储。如果明文存储的话,风险太大,且不说数据库的数据有被盗的风险,如果被服务端的相关人员特别是有数据库权限的恶意利用,那将是不可预估的风险。

一般情况下,我们都是通过哈希算法来加密密码并保存。

哈希算法也叫散列函数或摘要算法,它的作用是对任意长度的数据生成一个固定长度的唯一标识,也叫哈希值、散列值或消息摘要(后文统称为哈希值)。

哈希算法效果演示

哈希算法可以简单分为两类:

  1. 加密哈希算法
    :安全性较高的哈希算法,它可以提供一定的数据完整性保护和数据防篡改能力,能够抵御一定的攻击手段,安全性相对较高,但性能较差,适用于对安全性要求较高的场景。例如 SHA2、SHA3、SM3、RIPEMD-160、BLAKE2、SipHash 等等。
  2. 非加密哈希算法
    :安全性相对较低的哈希算法,易受到暴力破解、冲突攻击等攻击手段的影响,但性能较高,适用于对安全性没有要求的业务场景。例如 CRC32、MurMurHash3、SipHash 等等。

除了这两种之外,还有一些特殊的哈希算法,例如安全性更高的
慢哈希算法

关于哈希算法的详细介绍,可以看我写的这篇文章:
哈希算法和加密算法总结

目前,比较常用的是通过
MD5 + Salt
的方式来加密密码。盐(Salt)在密码学中,是指通过在密码任意固定位置插入特定的字符串,让哈希后的结果和使用原始密码的哈希结果不相符,这种过程称之为“加盐”。

不过,这种方式已经不被推荐,因为 MD5 算法的安全性较低,抗碰撞性差。详细介绍可以阅读我写的这篇文章:
简历别再写 MD5 加密密码了!
。你可以使用
安全性较高的加密哈希算法+ Salt(盐)
(例如 SHA2、SHA3、SM3,更高的安全性更强的抗碰撞性)或者直接使用
慢哈希
(例如 Bcrypt,更推荐这种方式)。

假如我们这里使用
SHA-256 + Salt
这种方式。

这里写了一个简单的示例代码:

String password = "123456";
String salt = "1abd1c";
// 创建SHA-256摘要对象
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update((password + salt).getBytes());
// 计算哈希值
byte[] result = messageDigest.digest();
// 将哈希值转换为十六进制字符串
String hexString = new HexBinaryAdapter().marshal(result);
System.out.println("Original String: " + password);
System.out.println("SHA-256 Hash: " + hexString.toLowerCase());

输出:

Original String: 123456
SHA-256 Hash: 424026bb6e21ba5cda976caed81d15a3be7b1b2accabb79878758289df98cbec

在这个例子中,服务端保存的就是密码“123456”加盐哈希之后的数据,也就是“424026bb6e21ba5cda976caed81d15a3be7b1b2accabb79878758289df98cbec” 。

当你输入密码登录之后,服务端会先把你的密码对应的盐取出,然后再去执行一遍获取哈希值的过程。如果最终计算出来的哈希值和保存在数据库中的哈希值一直,那就说明密码是正确的。否则的话,密码就不是正确的。

哈希算法的是不可逆的,你无法通过哈希之后的值再得到原值,这样的话,服务端也不知道你的原密码到底是什么,自然没办法告诉你原密码是什么。

那有的朋友又有疑问了,为什么很多网站改密码不可与原密码相同呢?这是过程实际和验证密码正确性一样的流程,计算一遍哈希值比较即可!

前言

对aop进行一个阶段性的总结。

正文

首先什么是aop呢?

那么首先看aop的解决什么样的问题。

public class Program
{
    public static void Main(string[] args)
    {
        
    }

    public void ChangePosition1()
    {
        // your operation
        SavePosition();
    }

    public void ChangePosition2()
    {
        // your operation
        SavePosition();
    }

    public void SavePosition()
    {
    }
}

看上面这个位置:
ChangePosition1 和 ChangePosition2

他们做完一些操作之后,需要做相同的操作,如上面所述,需要保存位置。

只有两个方法调用,那么不是啥子问题。

但是如果有大量的方法去调用这个东西,那么问题就出现了。

第一:重复性的工作
第二:出错率,每次都要写一遍,可能出现忘记的情况
第三:看着不优雅

那么在如此多的案例的情况下,人们都发现了规律。

比如说,在某个行为前做什么,在某个行为后做什么。

那么我们可以进行扩展:

public void Dosomething()
{
  // your operaion
}

aspect logging
{
   befor(dosomething is called)
   {
      Log.Write("enter dosomething")
   }

   after(dosomething is called)
   {
      Log.Write("after dosomething")
   }
}

aspect verification()
{
    befor(dosomething is called)
    {
      // your verification
    }
}

比如我们的验证和日志,可以通过这些aop去处理。

aop 全称是aspect-oriented programming 中文大多翻译过来是面向切面编程。

oriented 是面向,比如日志、验证,这些是aspect。

所以取了asepct-oriented programming 这个名字。

好的,那么现在了解了aop是什么东西,也了解了他的由来。

下面了解一下aop滥用的情况。

什么情况aop会滥用呢?

比如说:

public void setsomething()
{
}

aspect shutup
{
  after(something is called)
  {
    // shutup
  }
}

这种aop 就是反模式,不被推荐的。

原因是,比如我执行了setsomething之后,我setsomething 的意思是设置某一些东西,而没有shutup的含义。

但是最后却运行了该操作,这样难以维护和理解。

那么为什么logging 和 verification 能够被人所接受呢?

因为其没有破坏我们该代码的逻辑,的确setsomething做的事情就是它应该做的事情,不对执行逻辑造成影响。

深入一下aop是如何实现的呢?其实aop的原理很简单,但是优雅实现的却有一点点复杂。

  • 字节码操作


    • 优点
      :字节码操作可以在更细粒度的层面上操作代码,可以实现更灵活和精细的AOP功能。可以在编译期或运行期动态修改字节码,对目标类进行修改。
    • 缺点
      :实现相对复杂,需要对字节码结构有一定的了解。可能会影响代码的可读性和维护性。
  • 代理技术


    • 优点
      :代理技术相对简单易懂,可以快速实现AOP功能。可以通过代理对象来实现横切关注点的功能,不需要直接操作字节码。
    • 缺点
      :代理技术通常在运行时动态创建代理对象,可能会引入性能开销。对于一些高性能要求的场景,可能不太适合。

下面对这个分别进行举例:

先从好理解的代理开始:

public class LoggingAspect
{
    public void LogBefore(string methodName)
    {
        Console.WriteLine($"Logging before {methodName} execution");
    }
}

然后创建相应的代理:

public class UserServiceProxy<T> : DispatchProxy
{
    private T _decorated;
    
    private LoggingAspect _loggingAspect;
    
    protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
    {
        _loggingAspect.LogBefore(targetMethod.Name);
        
        return targetMethod.Invoke(_decorated, args);
    }
    
    public static T Create(T decorated)
    {
        object proxy = Create<T, UserServiceProxy<T>>();
        ((UserServiceProxy<T>)proxy)._decorated = decorated;
        ((UserServiceProxy<T>)proxy)._loggingAspect = new LoggingAspect();
        
        return (T)proxy;
    }
}

解释一下,这个create创建了什么,这个create 创建了两个类型的继承类。

也就是创建了动态类型。

public class UserService : IUserService
{
    public void Login()
    {
        Console.WriteLine("User logged in");
    }
}

现在是我们的代码实现了。

那么看下是怎么调用的。

public static void Main(string[] args)
{
    IUserService userService = new UserService();
    IUserService proxy = UserServiceProxy<IUserService>.Create(userService);
    proxy.Login();
}

这样就调用完成了。

看下效果。

这样就完成了相应的code。

至于为什么我们在使用框架的时候进行属性标记即可,那是因为框架帮我们把事情做了。

例如:

  1. 安装PostSharp
    :首先需要安装PostSharp NuGet包。

  2. 定义切面类

[Serializable]
public class LoggingAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        Console.WriteLine($"Logging before {args.Method.Name} execution");
    }
}
  1. 应用切面
    :在需要应用AOP的方法上添加切面标记。
public class UserService
{
    [LoggingAspect]
    public void Login()
    {
        Console.WriteLine("User logged in");
    }
}

至于这个框架是怎么实现的,原理就是利用MSBuilder。

MSBuild工具可以通过自定义任务(Custom Tasks)来实现预处理操作。通过编写自定义任务,可以在MSBuild构建过程中执行特定的预处理逻辑。以下是一个简单的示例,演示如何在MSBuild中使用自定义任务进行预处理:

  1. 创建自定义任务
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class CustomPreprocessTask : Task
{
    public override bool Execute()
    {
        // 在这里编写预处理逻辑
        Log.LogMessage("Custom preprocessing task executed.");
        return true;
    }
}
  1. 在项目文件中引用自定义任务

在项目文件(.csproj)中添加以下内容,引用自定义任务:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <UsingTask TaskName="CustomPreprocessTask" AssemblyFile="Path\\To\\CustomTask.dll" />
  
  <Target Name="CustomPreprocessTarget" BeforeTargets="Build">
    <CustomPreprocessTask />
  </Target>
</Project>
  1. 执行预处理操作

在构建项目时,MSBuild会执行自定义任务中定义的预处理逻辑。可以在
Execute
方法中编写任何需要的预处理代码,例如生成文件、修改配置等操作。

通过编写自定义任务并在项目文件中引用,可以利用MSBuild进行预处理操作。这样可以在构建过程中执行特定的逻辑,实现更灵活的构建流程。

代理模式,就是利用了msbuilder 预处理逻辑,在编译前进行了预处理。

那么字节码模式就是在msbuilder 编译后进行预处理。

using Mono.Cecil;
using Mono.Cecil.Cil;

public class LoggingAspect
{
    public void LogBefore()
    {
        Console.WriteLine("Logging before method execution");
    }
}

public class Program
{
    public static void Main()
    {
        AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly("YourAssembly.dll");
        ModuleDefinition module = assembly.MainModule;

        TypeDefinition type = module.Types.Single(t => t.Name == "UserService");
        MethodDefinition method = type.Methods.Single(m => m.Name == "Login");

        ILProcessor processor = method.Body.GetILProcessor();
        Instruction firstInstruction = method.Body.Instructions.First();

        // 在方法开头插入日志记录代码
        processor.InsertBefore(firstInstruction, processor.Create(OpCodes.Call, typeof(LoggingAspect).GetMethod("LogBefore")));

        assembly.Write("YourModifiedAssembly.dll");
    }
}

就是在我们程序编译完成后,进行在相应的位置进行注入程序。

下一节oop,关于aop解决一些实际问题的例子后续补齐。

Canvas介绍

Canvas

WPF(Windows Presentation Foundation)
中的一种面板控件,用于在XAML中布置子元素。它提供了绝对定位的能力,允许元素在自由的二维空间中放置。Canvas上的子元素可以通过指定绝对位置(Left和Top属性)来放置,也可以使用附加属性来指定相对于Canvas的位置。Canvas对于需要自由布局的场景非常有用,例如绘图应用程序或需要精确放置UI元素的情况。但是,使用Canvas布局时要注意,它不会自动调整子元素的位置或大小,因此需要手动管理子元素的布局。

image-20240416085443415

在Canvas上绘制矩形

在xaml定义一个Canvas:

 <StackPanel>
    <hc:Row Margin="0,20,0,0">
        <hc:Col Span="8">
            <Label Content="画矩形"></Label>
        </hc:Col>
        <hc:Col Span="8">
            <Button Style="{StaticResource ButtonPrimary}" Content="开始"
     Click="Button_Click_DrawRect"/>
        </hc:Col>
        <hc:Col Span="8">
            <Button Style="{StaticResource ButtonPrimary}" Content="清空"
                    Click="Button_Click_Clear"/>
        </hc:Col>
    </hc:Row>
    <Canvas Background="Azure" x:Name="myCanvas1" Height="400">
       
    </Canvas>
</StackPanel>

效果如下所示:

image-20240416085838561

绘制矩形:

 System.Windows.Shapes.Rectangle rectangle = new System.Windows.Shapes.Rectangle
 {
     Width = 100,
     Height = 100,
     Stroke = System.Windows.Media.Brushes.Blue,
     StrokeThickness = 1,                                      
 };

 Canvas.SetLeft(rectangle, 50);
 Canvas.SetTop(rectangle, 50);

 myCanvas1.Children.Add(rectangle);

System.Windows.Shapes.Rectangle

System.Windows.Shapes.Rectangle

WPF(Windows Presentation Foundation)
中的一个类,它表示一个矩形图形。

image-20240416093429075

以下是
Rectangle
类的一些主要属性:

属性名 类型 描述
Width Double 获取或设置元素的宽度。
Height Double 获取或设置元素的建议高度。
Stroke Brush 获取或设置 Brush,用于指定 Shape 边框绘制的方式。
StrokeThickness Double 获取或设置 Shape边框的宽度。
Fill Brush 获取或设置 Brush,它指定形状内部上色的方式。
 Canvas.SetLeft(rectangle, 50);
 Canvas.SetTop(rectangle, 50);

这两行代码是在设置Rectangle对象在Canvas中的位置。

  1. Canvas.SetLeft(rectangle, 50);:这行代码设置了
    rectangle
    对象在
    Canvas
    中的左边距。
    SetLeft
    是一个静态方法,它接受两个参数:第一个参数是要设置位置的对象,第二个参数是左边距的值。在这个例子中,rectangle对象的左边距被设置为50像素。
  2. Canvas.SetTop(rectangle, 50);:这行代码设置了
    rectangle
    对象在
    Canvas
    中的上边距。
    SetTop
    也是一个静态方法,它的工作方式与SetLeft相同,只是它设置的是上边距而不是左边距。在这个例子中,rectangle对象的上边距被设置为50像素。
 myCanvas1.Children.Add(rectangle);

这行代码将矩形添加到Canvas中。myCanvas1是Canvas的名称,Children.Add方法将矩形添加到Canvas的子元素中。

实现效果:

image-20240416094336410

也可以直接在xaml中写:

 <Canvas Background="Azure" x:Name="myCanvas1" Height="400">
     <Rectangle Width="100" Height="100"  Canvas.Left="50" Canvas.Top="50" Stroke="Blue" StrokeThickness="1"/>
 </Canvas>

效果与上述相同。

在Canvas上绘制圆

xaml写法:

 <Canvas Background="Azure" x:Name="myCanvas1" Height="400">
     <Ellipse Width="100" Height="100" Fill="Blue" Canvas.Left="50" Canvas.Top="50"/>
 </Canvas>

实现效果:

image-20240416094913617

cs写法:

 System.Windows.Shapes.Ellipse ellipse = new System.Windows.Shapes.Ellipse
 {
     Width = 100,
     Height = 100,                
     Fill = System.Windows.Media.Brushes.Blue
 };

 Canvas.SetLeft(ellipse, 50);
 Canvas.SetTop(ellipse, 50);

 myCanvas1.Children.Add(ellipse);

实现效果与上述相同。

在Canvas上绘制折线

xaml写法:

<Canvas Background="Azure" x:Name="myCanvas1" Height="400">
    <Polyline Points="10,10 50,50 100,20 150,70" Stroke="Blue" StrokeThickness="2"/>
</Canvas>

实现效果:

image-20240416095915008

cs写法:

 // 创建Polyline对象
 Polyline polyline = new Polyline();
 polyline.Points = new PointCollection()
 {
     new System.Windows.Point(10, 10),
     new System.Windows.Point(50, 50),
     new System.Windows.Point(100, 20),
     new System.Windows.Point(150, 70)
 };
 polyline.Stroke = System.Windows.Media.Brushes.Blue;
 polyline.StrokeThickness = 2;

 myCanvas1.Children.Add(polyline);

实现效果与上述相同。

在Canvas上绘制多边形

xaml写法:

 <Canvas Background="Azure" x:Name="myCanvas1" Height="400">
    <Polygon Points="350,200 250,100 300,250 " Fill="Red" Stroke="Blue" StrokeThickness="2"/>
</Canvas>

实现效果:

image-20240416101250384

cs写法:

 // 创建Polygon对象
 Polygon polygon = new Polygon();
 polygon.Points = new PointCollection()
 {
     new System.Windows.Point(350, 200),
     new System.Windows.Point(250, 100),
     new System.Windows.Point(300, 250)
 };
 polygon.Fill = System.Windows.Media.Brushes.Red;
 polygon.Stroke = System.Windows.Media.Brushes.Blue;
 polygon.StrokeThickness = 2;

 myCanvas1.Children.Add(polygon);

实现效果与上述相同。

在Canvas上绘制自定义路径

xaml写法:

<Canvas Background="Azure" x:Name="myCanvas1" Height="400">
    <Path Stroke="Blue" StrokeThickness="2">
        <Path.Data>
            <PathGeometry>
                <PathFigure StartPoint="10,10">
                    <LineSegment Point="50,50"/>
                    <LineSegment Point="100,20"/>
                    <LineSegment Point="150,70"/>
                </PathFigure>
            </PathGeometry>
        </Path.Data>
    </Path>
</Canvas>

实现效果:

image-20240416101923692

cs写法:

// 创建Path对象
Path path = new Path();
path.Stroke = System.Windows.Media.Brushes.Blue;
path.StrokeThickness = 2;

// 创建PathGeometry对象
PathGeometry pathGeometry = new PathGeometry();

// 创建PathFigure对象
PathFigure pathFigure = new PathFigure();
pathFigure.StartPoint = new System.Windows.Point(10, 10);

// 创建LineSegment对象并添加到PathFigure
pathFigure.Segments.Add(new LineSegment(new System.Windows.Point(50, 50), true));
pathFigure.Segments.Add(new LineSegment(new System.Windows.Point(100, 20), true));
pathFigure.Segments.Add(new LineSegment(new System.Windows.Point(150, 70), true));

// 将PathFigure添加到PathGeometry
pathGeometry.Figures.Add(pathFigure);

// 设置Path的Data属性为PathGeometry对象
path.Data = pathGeometry;

// 将path添加到myCanvas1中
myCanvas1.Children.Add(path);

实现效果与上述相同。

技术背景

Tkinter是一个Python自带的GUI框架,虽然现在主流的还是用pyqt的多一些,但是Tkinter在环境配置上可以节省很多工作,可以用来做一些小项目。如果是大型项目,用pyqt或者QT确实会更加专业一些。本文主要介绍一些简单的Tkinter的示例,比如文本框定义、标签定义和TreeView定义等。

窗口初始化

最基本的来说,类似于代码界的Hello Word,我们可以用Tkinter创建一个简单的窗体:

import tkinter as tk
# 主窗口
root = tk.Tk()
root.title('Title')
root.geometry('320x240')
# 启动运行
root.mainloop()

菜单栏

一般我们窗体上会有一个菜单栏,常用的菜单功能比如打开文件、文本操作等:

import tkinter as tk
root = tk.Tk()
root.title('Title')
root.geometry('320x240')
# 菜单
menubar = tk.Menu(root)
# 子菜单
submenu = tk.Menu(menubar, activebackground='blue')
submenu.add_command(label='Sub Menu 1', command=None)
submenu.add_command(label='Sub Menu 2', command=None)
# 把子菜单添加到主菜单中
menubar.add_cascade(label='Menu 1', menu=submenu)
menubar.add_command(label='Quit', command=root.destroy)
# 把主菜单配置到窗体中
root.config(menu=menubar)
root.mainloop()

文本框

import tkinter as tk
root = tk.Tk()
root.title('Title')
root.geometry('320x240')
menubar = tk.Menu(root)
submenu = tk.Menu(menubar, activebackground='blue')
submenu.add_command(label='Sub Menu 1', command=None)
submenu.add_command(label='Sub Menu 2', command=None)
menubar.add_cascade(label='Menu 1', menu=submenu)
menubar.add_command(label='Quit', command=root.destroy)
# 创建文本框,只能用字符数设置文本框的宽度
text_box = tk.Entry(root, bd=10)
# 设置默认文本内容
text_box.insert(0, 'Default Text')
# 占满当前布局
text_box.pack()
root.config(menu=menubar)
root.mainloop()

除了Entry文本框,还可以使用Text来定义文本框。如果使用Text定义文本框,定义时可以配置大小。如果对比这两个控件,最简单的来说就是,Entry适用于单行的输入(如登录界面的账号密码等),Text适用于多行的文本输入(文本编辑器)。

import tkinter as tk
root = tk.Tk()
root.title('Title')
root.geometry('320x240')
menubar = tk.Menu(root)
submenu = tk.Menu(menubar, activebackground='blue')
submenu.add_command(label='Sub Menu 1', command=None)
submenu.add_command(label='Sub Menu 2', command=None)
menubar.add_cascade(label='Menu 1', menu=submenu)
menubar.add_command(label='Quit', command=root.destroy)
# 设置文本框的大小
text_box = tk.Text(root, height=10, width=20)
text_box.insert('0.0', 'Default Text')
text_box.pack()
root.config(menu=menubar)
root.mainloop()

树结构

通俗的来看,树的结构就跟本地存放文件的目录一样,分有层级。如果广义的来看,凡是有索引的键值对结构,甚至是普通的矩阵形式,都可以用这种目录树的形式来显示。

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title('Title')
root.geometry('320x240')
menubar = tk.Menu(root)
submenu = tk.Menu(menubar, activebackground='blue')
submenu.add_command(label='Sub Menu 1', command=None)
submenu.add_command(label='Sub Menu 2', command=None)
menubar.add_cascade(label='Menu 1', menu=submenu)
menubar.add_command(label='Quit', command=root.destroy)
text_box = tk.Text(root, height=10, width=20)
text_box.insert('0.0', 'Default Text')
text_box.pack()
# 定义树结构
tree = ttk.Treeview(root)
# 一级节点
tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
# 二级节点
tree00 = tree.insert(tree0, 0, "Tree-0-0", text="Tree-0-0", values=("0-0"))
tree01 = tree.insert(tree0, 1, "Tree-0-1", text="Tree-0-1", values=("0-1"))
tree02 = tree.insert(tree0, 2, "Tree-0-2", text="Tree-0-2", values=("0-2"))
# 默认布局
tree.pack()
root.config(menu=menubar)
root.mainloop()

网格布局

上一个章节中的默认布局是上下布局,我们可以手动设定一个横向的grid布局,行和列分别用row和column来设定:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title('Title')
root.geometry('320x240')
menubar = tk.Menu(root)
submenu = tk.Menu(menubar, activebackground='blue')
submenu.add_command(label='Sub Menu 1', command=None)
submenu.add_command(label='Sub Menu 2', command=None)
menubar.add_cascade(label='Menu 1', menu=submenu)
menubar.add_command(label='Quit', command=root.destroy)
text_box = tk.Text(root, height=10, width=20)
text_box.insert('0.0', 'Default Text')
# 文本框放在第一行第一列
text_box.grid(row=0, column=0)
tree = ttk.Treeview(root)
tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
tree00 = tree.insert(tree0, 0, "Tree-0-0", text="Tree-0-0", values=("0-0"))
tree01 = tree.insert(tree0, 1, "Tree-0-1", text="Tree-0-1", values=("0-1"))
tree02 = tree.insert(tree0, 2, "Tree-0-2", text="Tree-0-2", values=("0-2"))
# 树形图放在第一行第二列
tree.grid(row=0, column=1)
root.config(menu=menubar)
root.mainloop()

需要注意的是,pack和grid两者是冲突的,不能同时使用。

按钮

按钮Button是一个用于事件触发的组件,定义形式较为简单:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title('Title')
root.geometry('320x240')
menubar = tk.Menu(root)
submenu = tk.Menu(menubar, activebackground='blue')
submenu.add_command(label='Sub Menu 1', command=None)
submenu.add_command(label='Sub Menu 2', command=None)
menubar.add_cascade(label='Menu 1', menu=submenu)
menubar.add_command(label='Quit', command=root.destroy)
text_box = tk.Text(root, height=10, width=20)
text_box.insert('0.0', 'Default Text')
text_box.grid(row=0, column=0)
# 定义按钮
button = tk.Button(root, text='Button', command=None)
# 把按钮放在第一行第二列
button.grid(row=0, column=1)
tree = ttk.Treeview(root)
tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
tree00 = tree.insert(tree0, 0, "Tree-0-0", text="Tree-0-0", values=("0-0"))
tree01 = tree.insert(tree0, 1, "Tree-0-1", text="Tree-0-1", values=("0-1"))
tree02 = tree.insert(tree0, 2, "Tree-0-2", text="Tree-0-2", values=("0-2"))
# 树形结构放在第一行第三列
tree.grid(row=0, column=2)
root.config(menu=menubar)
root.mainloop()

滚动条

虽然滚动条是一个很常见的功能,但是如果我们要在网格布局里面加滚动条,那就要把那些需要加滚动条的控件单独放到某个容器内,常用的有Widget和Frame。Frame是从Widget继承过来的,可以加一些边框阴影什么的,这里我们先用Frame来做一个简单示例:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title('Title')
root.geometry('320x240')
menubar = tk.Menu(root)
submenu = tk.Menu(menubar, activebackground='blue')
submenu.add_command(label='Sub Menu 1', command=None)
submenu.add_command(label='Sub Menu 2', command=None)
menubar.add_cascade(label='Menu 1', menu=submenu)
menubar.add_command(label='Quit', command=root.destroy)
# 定义一个指定大小的Frame
left_frame = tk.Frame(root, height=10, width=20)
text_box = tk.Text(left_frame, height=10, width=20)
text_box.insert('0.0', 'Default Text')
# 定义滚动条
scroll_text = tk.Scrollbar(left_frame)
# 定义滚动条的滚动方向
scroll_text.pack(side=tk.RIGHT, fill=tk.Y)
# 耦合滚动条与控件的视图
scroll_text.config(command=text_box.yview)
# 把滚动条添加到文本框的操作内
text_box.config(yscrollcommand=scroll_text.set)
# 这里是一个关键点,滚动条不能与grid共用,因此这里需要创建一个独立的容器才能加上滚动条
text_box.pack()
# 把Frame放在第一行第一列,这里放的就不是文本框控件了
left_frame.grid(row=0, column=0)
button = tk.Button(root, text='Button', command=None)
button.grid(row=0, column=1)
tree = ttk.Treeview(root)
tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
tree00 = tree.insert(tree0, 0, "Tree-0-0", text="Tree-0-0", values=("0-0"))
tree01 = tree.insert(tree0, 1, "Tree-0-1", text="Tree-0-1", values=("0-1"))
tree02 = tree.insert(tree0, 2, "Tree-0-2", text="Tree-0-2", values=("0-2"))
tree.grid(row=0, column=2)
root.config(menu=menubar)
root.mainloop()

面向对象的GUI

真正要做项目的时候,还是需要一个对象封装,便于局部的控制和更新,也方便功能维护与测试。一个应用对象应该包含GUI界面和操作函数,我们可以先对上面的这个简单案例做一个模块分离,构建一个简单的应用类型:

import tkinter as tk
from tkinter import ttk
# 自定义一个应用的对象
class Object:
    def __init__(self):
        # GUI界面只是应用的一个内置属性
        self.root = tk.Tk()
        self.root.title('Title')
        # 分模块初始化
        self.init_menu()
        self.init_text_box()
        self.init_button()
        self.init_tree()

    def init_menu(self):
        “”“菜单栏模块封装”“”
        menubar = tk.Menu(self.root)
        submenu = tk.Menu(menubar, activebackground='blue')
        submenu.add_command(label='Sub Menu 1', command=None)
        submenu.add_command(label='Sub Menu 2', command=None)
        menubar.add_cascade(label='Menu 1', menu=submenu)
        menubar.add_command(label='Quit', command=self.root.destroy)
        self.root.config(menu=menubar)

    def init_text_box(self):
        “”“文本框模块封装”“”
        left_frame = tk.Frame(self.root)
        text_box = tk.Text(left_frame, height=10, width=20)
        text_box.insert('0.0', 'Default Text')
        scroll_text = tk.Scrollbar(left_frame)
        scroll_text.pack(side=tk.RIGHT, fill=tk.Y)
        scroll_text.config(command=text_box.yview)
        text_box.config(yscrollcommand=scroll_text.set)
        text_box.pack()
        left_frame.grid(row=0, column=0)
    
    def init_button(self):
        “”“按钮模块封装”“”
        button = tk.Button(self.root, text='Button', command=None)
        button.grid(row=0, column=1)
    
    def init_tree(self):
        “”“树形结构模块封装”“”
        right_frame = tk.Frame(self.root)
        tree = ttk.Treeview(right_frame)
        tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
        tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
        tree00 = tree.insert(tree0, 0, "Tree-0-0", text="Tree-0-0", values=("0-0"))
        tree01 = tree.insert(tree0, 1, "Tree-0-1", text="Tree-0-1", values=("0-1"))
        tree02 = tree.insert(tree0, 2, "Tree-0-2", text="Tree-0-2", values=("0-2"))
        tree10 = tree.insert(tree1, 0, "Tree-1-0", text="Tree-1-0", values=("1-0"))
        tree100 = tree.insert(tree10, 0, "Tree-1-0-0", text="Tree-1-0-0\ntest\nmulti-line", values=("1-0-0"))
        tree.pack()
        right_frame.grid(row=0, column=2)

    def run(self):
        “”“运行启动”“”
        self.root.mainloop()

if __name__ == '__main__':
    my_app = Object()
    my_app.run()

在此基础之上我们可以实现一些功能函数,比如给按钮添加一些功能:

import tkinter as tk
from tkinter import ttk

class Object:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title('Title')
        self.init_menu()
        self.init_text_box()
        self.init_button()
        self.init_tree()

    def init_menu(self):
        menubar = tk.Menu(self.root)
        submenu = tk.Menu(menubar, activebackground='blue')
        submenu.add_command(label='Sub Menu 1', command=None)
        submenu.add_command(label='Sub Menu 2', command=None)
        menubar.add_cascade(label='Menu 1', menu=submenu)
        menubar.add_command(label='Quit', command=self.root.destroy)
        self.root.config(menu=menubar)

    def init_text_box(self):
        left_frame = tk.Frame(self.root)
        text_box = tk.Text(left_frame, height=10, width=20)
        text_box.insert('0.0', 'Default Text')
        scroll_text = tk.Scrollbar(left_frame)
        scroll_text.pack(side=tk.RIGHT, fill=tk.Y)
        scroll_text.config(command=text_box.yview)
        text_box.config(yscrollcommand=scroll_text.set)
        text_box.pack()
        left_frame.grid(row=0, column=0)
    
    def init_button(self):
        # 给按钮控件链接了一个update_tree的内部函数
        button = tk.Button(self.root, text='Button', command=self.update_tree)
        button.grid(row=0, column=1)
    
    def init_tree(self):
        right_frame = tk.Frame(self.root)
        tree = ttk.Treeview(right_frame)
        tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
        tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
        tree00 = tree.insert(tree0, 0, "Tree-0-0", text="Tree-0-0", values=("0-0"))
        tree01 = tree.insert(tree0, 1, "Tree-0-1", text="Tree-0-1", values=("0-1"))
        tree02 = tree.insert(tree0, 2, "Tree-0-2", text="Tree-0-2", values=("0-2"))
        tree10 = tree.insert(tree1, 0, "Tree-1-0", text="Tree-1-0", values=("1-0"))
        tree100 = tree.insert(tree10, 0, "Tree-1-0-0", text="Tree-1-0-0\ntest\nmulti-line", values=("1-0-0"))
        tree.pack()
        right_frame.grid(row=0, column=2)
    
    def update_tree(self):
        ““”该函数的主要功能,是替换树形结构的内容“””
        right_frame = tk.Frame(self.root)
        tree = ttk.Treeview(right_frame)
        tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
        tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
        tree.pack()
        right_frame.grid(row=0, column=2)

    def run(self):
        self.root.mainloop()


if __name__ == '__main__':
    my_app = Object()
    my_app.run()

点击按钮之前的树形结构显示:

点击按钮之后的树形结构显示:

标签

标签也比较容易理解,通常就是一些界面上不可变更的文字内容,用于标明各个控件的功能。

import tkinter as tk
from tkinter import ttk

class Object:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title('Title')
        self.init_menu()
        self.init_text_box()
        self.init_button()
        self.init_tree()
        init_text = "Welcome to tkinter."
        # 用一个文本参数来初始化底部标签控件,类似于提示信息或者日志信息的内容显示
        self.init_label(init_text)

    def init_menu(self):
        menubar = tk.Menu(self.root)
        submenu = tk.Menu(menubar, activebackground='blue')
        submenu.add_command(label='Sub Menu 1', command=None)
        submenu.add_command(label='Sub Menu 2', command=None)
        menubar.add_cascade(label='Menu 1', menu=submenu)
        menubar.add_command(label='Quit', command=self.root.destroy)
        self.root.config(menu=menubar)

    def init_text_box(self):
        left_frame = tk.Frame(self.root)
        text_box = tk.Text(left_frame, height=10, width=20)
        text_box.insert('0.0', 'Default Text')
        scroll_text = tk.Scrollbar(left_frame)
        scroll_text.pack(side=tk.RIGHT, fill=tk.Y)
        scroll_text.config(command=text_box.yview)
        text_box.config(yscrollcommand=scroll_text.set)
        text_box.pack()
        left_frame.grid(row=0, column=0)
    
    def init_button(self):
        button = tk.Button(self.root, text='Button', command=self.update_tree)
        button.grid(row=0, column=1)
    
    def init_tree(self):
        right_frame = tk.Frame(self.root)
        tree = ttk.Treeview(right_frame)
        tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
        tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
        tree00 = tree.insert(tree0, 0, "Tree-0-0", text="Tree-0-0", values=("0-0"))
        tree01 = tree.insert(tree0, 1, "Tree-0-1", text="Tree-0-1", values=("0-1"))
        tree02 = tree.insert(tree0, 2, "Tree-0-2", text="Tree-0-2", values=("0-2"))
        tree10 = tree.insert(tree1, 0, "Tree-1-0", text="Tree-1-0", values=("1-0"))
        tree100 = tree.insert(tree10, 0, "Tree-1-0-0", text="Tree-1-0-0\ntest\nmulti-line", values=("1-0-0"))
        tree.pack()
        right_frame.grid(row=0, column=2)
    
    def init_label(self, text):
        “”“添加标签控件,放在第二行”“”
        information = tk.Label(self.root, text=text)
        information.grid(row=1)
    
    def update_tree(self):
        right_frame = tk.Frame(self.root)
        tree = ttk.Treeview(right_frame)
        tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
        tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
        tree.pack()
        right_frame.grid(row=0, column=2)

    def run(self):
        self.root.mainloop()

if __name__ == '__main__':
    my_app = Object()
    my_app.run()

用标签这个控件,还可以结合可变的交互式输入内容,例如我们用到的文本框里面的文字,做一个信息提示小组件

import tkinter as tk
from tkinter import ttk

class Object:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title('Title')
        self.init_menu()
        # 初始化文本框对象
        self.text_box = self.init_text_box()
        self.init_button()
        self.init_tree()
        init_text = "Welcome to tkinter."
        # 初始化标签对象
        self.label=self.init_label(init_text)

    def init_menu(self):
        menubar = tk.Menu(self.root)
        submenu = tk.Menu(menubar, activebackground='blue')
        submenu.add_command(label='Sub Menu 1', command=None)
        submenu.add_command(label='Sub Menu 2', command=None)
        menubar.add_cascade(label='Menu 1', menu=submenu)
        menubar.add_command(label='Quit', command=self.root.destroy)
        self.root.config(menu=menubar)

    def init_text_box(self):
        left_frame = tk.Frame(self.root)
        text_box = tk.Text(left_frame, height=10, width=20)
        text_box.insert('0.0', 'Default Text')
        scroll_text = tk.Scrollbar(left_frame)
        scroll_text.pack(side=tk.RIGHT, fill=tk.Y)
        scroll_text.config(command=text_box.yview)
        text_box.config(yscrollcommand=scroll_text.set)
        text_box.pack()
        left_frame.grid(row=0, column=0)
        # 返回文本框对象
        return text_box
    
    def init_button(self):
        button = tk.Button(self.root, text='Button', command=self.update_tree)
        button.grid(row=0, column=1)
    
    def init_tree(self):
        right_frame = tk.Frame(self.root)
        tree = ttk.Treeview(right_frame)
        tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
        tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
        tree00 = tree.insert(tree0, 0, "Tree-0-0", text="Tree-0-0", values=("0-0"))
        tree01 = tree.insert(tree0, 1, "Tree-0-1", text="Tree-0-1", values=("0-1"))
        tree02 = tree.insert(tree0, 2, "Tree-0-2", text="Tree-0-2", values=("0-2"))
        tree10 = tree.insert(tree1, 0, "Tree-1-0", text="Tree-1-0", values=("1-0"))
        tree100 = tree.insert(tree10, 0, "Tree-1-0-0", text="Tree-1-0-0\ntest\nmulti-line", values=("1-0-0"))
        tree.pack()
        right_frame.grid(row=0, column=2)
    
    def init_label(self, text):
        information = tk.Label(self.root, text=text)
        information.grid(row=1)
        return information
    
    def update_tree(self):
        right_frame = tk.Frame(self.root)
        tree = ttk.Treeview(right_frame)
        tree0 = tree.insert("", 0, "Tree-0", text="Tree-0", values=("0"))
        tree1 = tree.insert("", 1, "Tree-1", text="Tree-1", values=("1"))
        tree.pack()
        right_frame.grid(row=0, column=2)
        # 在按钮的功能函数中增加对标签控件的刷新
        self.update_label()
    
    def update_label(self):
        text = self.text_box.get("1.0", "end")
        # 销毁原本的标签内容
        self.label.after(0, self.label.destroy())
        # 输入新的标签内容
        self.label = self.init_label(text)

    def run(self):
        self.root.mainloop()

if __name__ == '__main__':
    my_app = Object()
    my_app.run()

第一次点击按钮

更新文本内容后,第二次点击按钮

总结概要

本文主要介绍一些Python的Tkinter GUI框架的常用功能模块,包含基本窗口的创建、菜单栏、文本框、TreeView、按钮、滚动条、标签的设定等,另外包含了一些面向对象的GUI的简单示例。总的来说,Tkinter加上第三方的ttk,基本的GUI功能是都具备的,可以用来实现一些简单的小项目。对于大的项目来说,用PyQT/QT可能会是一个更加专业的选择。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/tkinter1.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

1)instantbox 介绍

GitHub:
https://github.com/instantbox/instantbox

instantbox
是一款非常实用的项目,它能够让你在几秒内启动一个主流的 Linux 系统,随起随用,支持 Ubuntu,CentOS, Arch Linux,Debian,Fedora 和 Alpine,通过 WebShell 访问,简单快捷,适合于演示、测试、体验等场合。也就是说可以通过浏览器页面来操作 Linux 系统。

2)instantbox 特点

  1. 快速启动:
    项目利用虚拟化技术,使我们能够在几分钟内启动一个全新的 Linux 环境。无需担心繁琐的安装和配置过程,就可以立即开始演示。
  2. 灵感激发:
    在这个干净的环境中,我们可以充分发挥创造力和灵感,展示 Linux 的强大功能和无限可能性。
  3. 跨设备管理服务器:
    利用这个项目,我们可以从任何设备轻松管理服务器。无论是在我们的个人电脑、笔记本电脑还是移动设备上,都可以随时访问和管理服务器。
  4. 尝试开源项目:
    在这个干净的 Linux 环境中,我们可以尝试使用各种开源项目。学生们可以探索不同的应用程序、工具和开发框架,深入了解开源技术的魅力。
  5. 测试软件性能:
    通过我们提供的资源限制功能,您可以在不同的资源限制下测试软件的性能。这有助于了解软件在不同环境下的表现,并进行性能优化。

3)应用场景

  • 演讲时,临时需要一个干净的 Linux 环境,可以尝试使用 instantbox 为观众做演示
  • 在外边没有携带设备时,instantbox 可以在任何一台设备上 对服务器进行管理 类似于跳板机
  • 看到 GitHub 上某个非常感兴趣的项目想要尝试,却因为该项目运行在 Linux 而望而却步?instantbox 可以立刻获得一个干净的环境
  • instantbox 由于使用 docker 作为支持,所以我们使用了 cgroups 来对性能进行管理,如果想 测试的某个应用在某个性能下是否能够运行,使用 instantbox 是一个非常好的选择

4)检查 Docker 环境

4.1)安装 Docker 软件

# 高版本 Docker 安装
curl -fsSL https://get.docker.com/ | sh
# 关闭防火墙
systemctl disable --now firewalld
setenforce 0
# 启用 Docker
systemctl enable --now docker

4.2)检查 Docker 服务

systemctl status docker

image.png

4.3)开启 IPv4 forwarding

echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
systemctl restart network
sysctl net.ipv4.ip_forward

5)安装 Docker-compose

5.1)下载 Docker-Compose 软件包

curl -L https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose

5.2)Docker-Compose 增加执行权限

chmod +x /usr/local/bin/docker-compose

5.3)检查 Docker-Compose 版本

docker-compose -v

image.png

6)部署 instantbox

6.1)执行脚本

在当前路径下创建 instantbox 目录并下载对应的 Docker-Compose 文件。

mkdir instantbox && cd $_

bash <(curl -sSL https://raw.githubusercontent.com/instantbox/instantbox/master/init.sh)
# 一路回车
Welcome to instantbox, please wait...

docker is installed
docker-compose is installed
Enter your IP (optional):

Choose a port (default: 8888):

You're all set!
Run 'docker-compose up -d' then go to http://localhost:8888 on your browser.

image.png

6.2)启用服务

docker-compose up -d

image.png

6.3)验证服务

docker ps -a

image.png

7)访问 instantbox

使用浏览器访问前台:
http://服务器IP地址:8888

image.png

页面如下,我们选择一个常用的系统,点击 Next

image.png

然后设置好 CPU 核数,内存,端口等信息,点击 Create 即可。

image.png

稍等 1 - 2 分钟后,我们直接打开 Web Shell。就可以操作了,非常奈斯!

image.png
image.png

因为下载的 Linux 版本是 对应发行版本的精简系统,除了重要内置工具自带了,其他工具只有在我们需要用的时候进行安装了。当然这也是非常简单的事情啦。