[置顶]F#学习之路(5)元组类型

本文深入探讨F#元组类型的功能特性,包括定义、比较、与模式匹配的结合,以及在函数式编程中的互操作性。详细解释了元组类型在F#中的用法与C#的兼容性,强调了其在数据组合与分离方面的优势。

     元组类型,表示一组有序数据类型的集合。F#通过支持元组类型,方便了我们定义临时数据结构,而不需要为了临时的数据专门定义一个数据类型。

     一、元组的定义:     

 

  let  tuple_2=( 1 , 2

 
let  tuple_3=( " F# " , 1.9 , " F# Function Language "

   

     在F#中元组使用小括号,元素之间逗号分隔来定义。元组元素可以是任何类型。

     上面代码中,tuple_2的类型是int*int,而tuple_3的类型为string*float*string。元组类型使用元组元素类型乘号连接的方式。

   

     元组类型可以作为函数的输入参数,也可以作为函数的返回值。    

 

let  test (a,b,c) = 
    a+
1 ,b+ 2 ,c+ 3  

 

     上面函数的类型为 int*int*int->int*int*int

   

     元组类型虽然方便我们组织一组数据类型,而无需专门定义一个数据结构。但很显然他不能很好地帮助我们理解数据结构的语义,尤其是互操作时作为函数返回值的情况下。给元组类型取一个别名,能够部分缓解这个问题。

 

type  FSharpDesc= string * float * string  

let  getFSharpDesc ():FSharpDesc =
    (
" F# " , 1.9 , " F# Function Language "

   

     二、元组类型的比较    

     在F#中元组类型可以进行比较,前提当然是同类型的元组类型。所谓同类型,是指元组的元素长度相等,元素类型相同。F#从前至后依次比较。    

 

do  printfn  " %b "  (( 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ) <( 2 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 )) 

   

     打印的结果 为 true    

   

     三、元组与模式匹配 

      模式匹配在函数式语言中经常出现,其功能强大。比命令式语言中switch强大的多。不仅可以匹配基本类型、元组类型、列表类型、记录类型,还可以用于类型检测。在F#中还允许自定义类型的模式匹配,通过活动模式(Active Pattern)可以做到这一点。

     

let  tuple_7=( 1 , 2 , 3 , 4 , 5 , 6 ,( 7 , 8 , 9 )) 

let  one,two,three,four,five,six,seven=tuple_7    

[one;two;three;four;five;six;] @ (
match  seven  with  t1,t2,t3  -> [t1;t2;t3] ) 
| > List.iteri 
   (
fun  index item  -> printfn  " %d,%A "  index item) 

 

    元组类型不仅可以使用常用的模式匹配方法。 

   

  match  seven  with  t1,t2,t3  -> [t1;t2;t3] 

    

     还可以使用隐式的模式匹配方法来分解元组

    

let  one,two,three,four,five,six,seven=tuple_7

       

    这种隐式的模式匹配大大方便了我们使用元组类型的分解。上面的代码使用了列表,列表将在下一篇博客中讨论。

 

     四、元组类型的互操作

     在学习F#函数时,我们知道F#函数的输入参数是不需要括号的。

     

let  f1 a b c =
  a+b+c

let  f2 (a,b,c) =
  a+b+c

 

     上面的代码中f1和f2的类型是不一样的。

     f1的函数类型是int->int->int->int,而f2的函数类型是int*int*int->int。

     我把f1函数称为柯里化函数风格,而把f2函数称为元组函数风格。

     之所以称f1为柯里化的函数风格,是因为f1可以进行柯里化的函数调用,可以部分地传入参数值。而f2则必须全部传入。    

     两种函数风格的适用场景是什么了?

     柯里化的函数很显然比较适合扮演高阶函数。根据我目前所理解,在不讨论互操作的情况下,元组类型风格的函数总是可以通过柯里化的函数风格代替的。在不需要互操作的情况下,应该优先使用柯里化的函数风格,因为柯里化的函数可以更好的组合,从这个角度来说,柯里化的函数风格可以很好的组合代码,而元组类型的函数风格则用来组合数据。

     为什么说在互操作场景中优先使用元组类型的函数风格?

     回答上面的问题,则要讨论一下元组类型真正的类型是什么?什么叫元组类型真正的类型呀,元组类型不就是元组类型吗?呵呵,绕起来了。

     

let  tuple_3=( 1 , 2 , 3

 

     上面的tuple_3元组类型是int*int*int,这是对的。但这只是表面,实质上它的真实类型是Tuple<int,int,int>,F#编译器做了手脚。Tuple类型定义了一个泛型的记录类型,它有三个元素。

 

  type  Tuple< ' a, ' b, ' c> = 
    { Item1:  ' a; Item2:  ' b; Item3:  ' c }

     

     在F#中,一共定义了六个泛型版的元组类型。从2个元素到七个元素的元组类型。很显然,元组类型最少两个元素,最多没有限制,理论上肯定是只要内存足够就可以了。F#如何处理超过七个元素的元组类型的了。根据我的测试,发现如果超过七个元素,超过的元素将变成一个嵌套的元组的成员,依此类推。举个例子:

 

let  TestTupleWithGreaterThanSeven(a, b, c ) =
   (a,b,c,a+b,a+c,b+c,a+b+c,
10 *a+ 2 *b+c)

    

      上面的F#函数对应的c#方法签名是:

 

public   static  Tuple < int int int int int int , Tuple < int int >>  TestTupleWithGreaterThanSeven( int  a,  int  b,  int  c);

     

     注意上面的讨论是针对函数返回值的。对于函数输入参数并不适用,我们要分别讨论。

     对于函数来说,元组类型的函数风格与柯里化的函数风格,对于c#来说,并没有区别。或者说,柯时化的函数风格和元组类型的函数风格都会转化为c#方法风格。

 

let  TestTuple(a, b, c,d,e,f,g,h ) =
    a+b+c+d+e+f+g+h

let  TestCurrying a b c d e f g h =
    a+b+c+d+e+f+g+h

 

     对应的c#方法签名:

     

  public   static   int  TestCurrying( int  a,  int  b,  int  c,  int  d,  int  e,  int  f,  int  g,  int  h);
 
public   static   int  TestTuple( int  a,  int  b,  int  c,  int  d,  int  e,  int  f,  int  g,  int  h);

 

     一个有趣的例子:

 

let  tuple_7=( 1 , 2 , 3 , 4 , 5 , 6 ,( 7 , 8 , 9 ))

let  tuple_9=( 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 )

     上面的tuple_7和tuple_9在F#中是完全不同的类型,但对于c#来说,却是完全等效的。

 

     但当我们在类型上使用这两种风格的函数时就发生了变化,这种变化还很大。下面我就定义一个记录类型:

     

type Order = 

   {ID:string;Name:string;CreateDate:System.DateTime;Remark:string option}
    
with 
    
static member CreateWithCurrying id name createdate remark =
        {ID=id;Name=name;CreateDate=createdate;Remark=remark}
    
static member CreateWithTuple(id, name, createdate, remark) =
       {ID=id;Name=name;CreateDate=createdate;Remark=remark}

     

     我创建了一个记录类型,叫Order,并使用静态成员方法创建类型实例。

     c#签名如下:

     

  public   static  FastFunc < string , FastFunc < DateTime, FastFunc < Option < string > , TupleTest.Order >>>  CreateWithCurrying( string  id);
        
public   static  TupleTest.Order CreateWithTuple( string  id,  string  name, DateTime createdate, Option < string >  remark);
    

          

     这意味着什么,这意味着当我们在F#中使用柯里化风格的函数作为公开接口时,不是c#程序员习惯的调用方式。而采用元组风格的函数与c#程序员的习惯是一致的。

     在F#中元组的存在,使得接受多值的返回值时,F#比C#处理方式漂亮的多。 

 

match  DateTime.TryParse( " 2008-8-25 " with
|   true , d  ->  printfn  " %s "  (d.ToString())
|  _  ->  printfn  " datetime parse error "

         

 

     五、总结:

     (1) 元组类型一般用做临时数据的容器。业务数据结构应该选用记录类型或类、结构。

     (2) 当需要与其他.net语言互操作时,元组类型用做方法或函数的输入参数,有更好的兼容性,除非你的确需要柯里化的函数风格。在互操作时,元组类型不要作为函数的返回值,如果使用元组类型作为函数返回值,互操作的语言就必须引用F#的特定函数库。

     (3) 元组类型与模式匹配语法相结合,可以很好的组合、拆分。

       

     下一篇:F#学习之路(6)列表类型 

 

基于以下代码修改实现,桌面会话窗口不在显示的最顶层但锁屏倒计时登陆界面必须要弹出在最顶层。仅给出需修改部分的代码即可。 import tkinter as tk from tkinter import simpledialog, messagebox, scrolledtext, ttk import time import threading import json import os import platform from cryptography.fernet import Fernet from pynput import mouse, keyboard import sys import subprocess import csv import pandas as pd # 获取当前脚本所在目录作为基础路径 if getattr(sys, 'frozen', False): # 打包后的可执行文件路径 BASE_DIR = os.path.dirname(sys.executable) else: # 脚本文件路径 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # 配置参数(常量使用大写) IDLE_THRESHOLD = 5 # 3分钟空闲时间(秒) COUNTDOWN_DURATION = 10 # 锁屏倒计时(秒) KEY_FILE = os.path.join(BASE_DIR, "system_secret.key") # 加密密钥文件改为绝对路径 LOG_FILE = os.path.join(BASE_DIR, "secure_usage_log.log") # 操作日志文件路径改为绝对路径 ADMIN_PASSWORD = "123" # 管理员密码 LOCK_SCREEN_BG = "#000000" # 锁屏背景色(黑色) LOGIN_BG = "#2c3e50" # 登录界面背景色(深蓝色) BUTTON_BG = "#3498db" # 按钮背景色(亮蓝色) USER_FILE = os.path.join(BASE_DIR, "培训通过人员单.csv") # 改为绝对路径 USER_MANAGEMENT_PASSWORD = ADMIN_PASSWORD # 用户管理密码设置为管理员密码 # 确保BASE_DIR存在 os.makedirs(BASE_DIR, exist_ok=True) # 培训通过人员单(工号:密码) # 修改全局用户加载逻辑 def load_trained_users(file_path): """ 从CSV文件加载培训通过人员单 文件格式要求:工号,密码 返回字典 {工号: 密码} """ users = {} try: # 检查文件是否存在 if not os.path.exists(file_path): print(f"用户文件 {file_path} 不存在,创建初始管理员账号") # 创建只包含管理员的初始用户单 users = {"ADMIN": ADMIN_PASSWORD} # 保存加密的用户单 save_user_file(users, file_path) return users # 加载加密密钥 if not os.path.exists(KEY_FILE): key = Fernet.generate_key() with open(KEY_FILE, "wb") as f: f.write(key) with open(KEY_FILE, "rb") as f: key = f.read() cipher = Fernet(key) # 解密用户数据 with open(file_path, "rb") as f: encrypted_data = f.read() decrypted_data = cipher.decrypt(encrypted_data) return json.loads(decrypted_data) except Exception as e: print(f"加载用户单错误: {str(e)}") # 返回只包含管理员的用户单 return {"ADMIN": ADMIN_PASSWORD} # 添加保存用户文件的函数 def save_user_file(users, file_path): """保存用户字典到CSV文件""" try: # 确保密钥存在 if not os.path.exists(KEY_FILE): key = Fernet.generate_key() with open(KEY_FILE, "wb") as f: f.write(key) with open(KEY_FILE, "rb") as f: key = f.read() cipher = Fernet(key) # 加密用户数据 encrypted_data = cipher.encrypt(json.dumps(users).encode()) # 新加 os.makedirs(os.path.dirname(file_path) or '.', exist_ok=True) with open(file_path, "wb") as f: f.write(encrypted_data) return True except Exception as e: print(f"保存用户文件时出错: {str(e)}") return False # 从文件加载培训通过人员单 TRAINED_USERS = load_trained_users(USER_FILE) # 确保管理员账户存在 if "ADMIN" not in TRAINED_USERS: TRAINED_USERS["ADMIN"] = ADMIN_PASSWORD save_user_file(TRAINED_USERS, USER_FILE) class SecureDesktopMonitor: def __init__(self): # 创建主窗口 self.root = tk.Tk() # 创建主窗口对象 self.root.title("安全桌面监控系统") # 设置窗口标题 self.root.geometry("800x600") # 初始窗口尺寸 self.root.configure(bg=LOGIN_BG) # 设置背景色 # 设置窗口在最顶层 self.root.attributes("-topmost", True) # 系统状态变量,防止用户绕过监控 self.current_user = None # 当前登录用户(未登录时为None) self.login_time = 0 # 登陆时间(初始为0) self.last_activity = time.time() # 最后活动时间(初始化为当前时间) self.idle_timer = None # 空闲检测定时器(用于推迟锁屏) self.countdown_timer = None # 倒计时定时器(锁屏前提示) self.listening = False # 监听器状态标志 self.is_locked = False # 界面锁屏状态(初始为True,即未登录时锁定) # 创建鼠标键盘监听器 self.mouse_listener = mouse.Listener(on_move=self.activity_detected) # 鼠标移动触发活动检测 self.keyboard_listener = keyboard.Listener(on_press=self.activity_detected) # 键盘按键触发活动检测 # 显示初始登录界面 self.show_login_screen() # Tkinter事件循环(阻塞式,保持程序运行),mainloop()是Tkinter的核心,用于处理用户事件(如点击、输入) self.root.mainloop() def show_login_screen(self): """显示登录界面""" self.clear_screen() # 清空当前屏幕所有内容 self.is_locked = True # 标记系统为锁定状态 # 设置全屏模式(防止用户切换窗口) self.root.attributes('-fullscreen', True) # 创建居中主框架 main_frame = tk.Frame(self.root, bg=LOGIN_BG) # bg颜色深蓝色 main_frame.place(relx=0.5, rely=0.5, anchor=tk.CENTER) # 使用place布局精确居中 # 系统标题标签 title = tk.Label( main_frame, text="安全桌面登录", font=("Arial", 24, "bold"), # 字体族、大小、粗体 fg="white", # 前景色(白色) bg=LOGIN_BG # 背景色与界面一致 ) title.pack(pady=20) # 打包布局,垂直间距20像素 # 登录表单框架(使用grid布局对齐输入框) form_frame = tk.Frame(main_frame, bg=LOGIN_BG) form_frame.pack(pady=20) # 工号输入标签输入框 tk.Label( form_frame, # 框架格式 text="工号:", font=("黑体", 14), fg="white", bg=LOGIN_BG ).grid(row=0, column=0, padx=10, pady=10, sticky="e") self.id_entry = tk.Entry(form_frame, font=("Arial", 14), width=20) # 单行文本输入框 self.id_entry.grid(row=0, column=1, padx=10, pady=10) self.id_entry.focus_set() # 自动聚焦到工号输入框(提升用户体验) # 密码输入标签输入框(内容显示为*号) tk.Label( form_frame, text="密码:", font=("黑体", 14), fg="white", bg=LOGIN_BG ).grid(row=1, column=0, padx=10, pady=10, sticky="e") self.pw_entry = tk.Entry(form_frame, show="*", font=("Arial", 14), width=20) # show="*" 隐藏输入 self.pw_entry.grid(row=1, column=1, padx=10, pady=10) # 放置位置 # 绑定回车键(用户输入密码后按回车可直接登录) self.pw_entry.bind("<Return>", lambda event: self.authenticate_user()) # 登录按钮 login_btn = tk.Button( form_frame, text="登 录", command=self.authenticate_user, # 点击时触发认证方法 font=("黑体", 18, "bold"), bg=BUTTON_BG, # 按钮背景色 fg="white", # 按钮文字颜色 width=15, height=2 ) login_btn.grid(row=2, column=0, columnspan=2, pady=20) # 放置位置跨两列居中 # 系统信息 sys_info = tk.Label( main_frame, text="用户使用时长监测系统 | 仅限授权人员使用", font=("黑体", 10), fg="#bdc3c7", bg=LOGIN_BG ) sys_info.pack(side=tk.BOTTOM, pady=10) def authenticate_user(self): """验证用户身份(首次登录设置密码)""" user_id = self.id_entry.get().strip() # 获取用户id密码 password = self.pw_entry.get().strip() # 检查是否是管理员登录且用户单只有管理员 is_only_admin = len(TRAINED_USERS) == 1 and "ADMIN" in TRAINED_USERS # 检查用户是否存在 if user_id in TRAINED_USERS: # 如果是管理员且用户单为空,直接进入用户管理界面 if is_only_admin and user_id == "ADMIN": self.current_user = user_id self.login_time = time.time() self.last_activity = self.login_time self.is_locked = False self.manage_users() # 直接进入用户管理 return # 处理首次登录(密码为空) if TRAINED_USERS[user_id] == "": self.set_initial_password(user_id) return # 如果id密码匹配,则正常启动桌面会话 if TRAINED_USERS[user_id] == password: self.current_user = user_id self.login_time = time.time() self.last_activity = self.login_time self.is_locked = False self.start_desktop_session() # 开始桌面会话函数 return else: messagebox.showerror("访问拒绝", "输入密码有误,请重新输入!") self.pw_entry.delete(0, tk.END) return # 如果用户单只有管理员,给出特定提示 if is_only_admin: messagebox.showerror("访问拒绝", "当前只有管理员可以登录系统,请使用管理员账号登录") else: messagebox.showerror("访问拒绝", "未授权操作!禁止访问系统!") # 清空密码框 self.pw_entry.delete(0, tk.END) # 新用户首次登陆设置密码 def set_initial_password(self, user_id): """新用户首次登录设置密码""" dialog = tk.Toplevel(self.root) dialog.title("设置初始密码") dialog.geometry("300x200") dialog.transient(self.root) dialog.grab_set() # 窗口居中设置 screen_width = dialog.winfo_screenwidth() screen_height = dialog.winfo_screenheight() x = (screen_width - 300) // 2 y = (screen_height - 200) // 2 dialog.geometry(f"300x200+{x}+{y}") tk.Label(dialog, text=f"欢迎新用户 {user_id}").pack(pady=(10, 0)) tk.Label(dialog, text="请设置您的初始密码:").pack(pady=(10, 0)) password_entry = tk.Entry(dialog, show="*", width=20) password_entry.pack() tk.Label(dialog, text="确认密码:").pack(pady=(10, 0)) confirm_entry = tk.Entry(dialog, show="*", width=20) confirm_entry.pack() def save_password(): password = password_entry.get().strip() confirm = confirm_entry.get().strip() if not password: messagebox.showerror("错误", "密码不能为空", parent=dialog) return if password != confirm: messagebox.showerror("错误", "两次输入的密码不一致", parent=dialog) return # 更新密码 TRAINED_USERS[user_id] = password save_user_file(TRAINED_USERS, USER_FILE) # 完成设置 dialog.destroy() messagebox.showinfo("成功", "密码设置成功!") # 自动登录 self.current_user = user_id self.login_time = time.time() self.last_activity = self.login_time self.is_locked = False self.start_desktop_session() tk.Button(dialog, text="保存", command=save_password).pack(pady=10) def start_desktop_session(self): """开始桌面会话""" self.clear_screen() self.root.attributes('-fullscreen', False) # 设置悬浮窗口属性 self.root.overrideredirect(True) # 移除窗口边框标题栏 self.root.attributes("-topmost", False) # 取消置顶显示 self.root.geometry("300x300") self.root.title(f"使用中 - 用户: {self.current_user}") # 关键修改:设置悬浮窗口背景色透明度 self.root.configure(bg="#f0f0f0") # 浅灰色背景 # 移除窗口关闭功能 self.root.protocol("WM_DELETE_WINDOW", lambda: None) # 停止可能存在的旧监听器 self.stop_activity_monitoring() # 启动新的监听 self.start_activity_monitoring() # 显示桌面内容 desktop_frame = tk.Frame(self.root, bg="#f0f0f0") # 设置内部框架同样背景色 desktop_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=50) # expand可将组件由其势力范围扩大到扩展范围 # 欢迎信息(简化显示) welcome_msg = tk.Label( desktop_frame, text=f"用户: {self.current_user}", font=("Arial", 14, "bold"), pady=10 ) welcome_msg.pack() # 使用时间显示 self.time_label = tk.Label( desktop_frame, text="使用时间: 00:00", font=("黑体", 10) ) self.time_label.pack(pady=5) # 手动锁定按钮 lock_btn = tk.Button( desktop_frame, text="锁定系统", command=self.lock_system, font=("黑体", 10), bg="#e74c3c", fg="white" ) lock_btn.pack(pady=10) # ⭐管理员按钮组框架 admin_frame = tk.Frame(desktop_frame) admin_frame.pack(pady=5) # 管理员查看记录按钮(仅管理员可见) if self.current_user == "ADMIN": admin_btn = tk.Button( admin_frame, text="查看使用记录", command=self.admin_view, font=("黑体", 10), bg="#F5FFFA", fg="#e74c3c" ) admin_btn.pack(side=tk.LEFT, padx=5) # 新增:管理用户单按钮(仅管理员可见) manage_users_btn = tk.Button( admin_frame, text="管理用户单", command=self.manage_users, font=("黑体", 10), bg="#F5FFFA", fg="#e74c3c" ) manage_users_btn.pack(side=tk.LEFT, padx=5) # 启动鼠标键盘事件监听 self.start_activity_monitoring() # 鼠标键盘监听函数 # 开始空闲检测 self.start_idle_monitor() # 开始更新使用时间显示 self.update_usage_time() # 管理员管理培训通过单⭐ def manage_users(self): """管理员管理用户单""" # 如果是首次登录(只有管理员),跳过密码验证 if len(TRAINED_USERS) > 1 or "ADMIN" not in TRAINED_USERS: # 验证管理员密码 password = simpledialog.askstring("❗", "请输入管理员密码:", show='*') if password != ADMIN_PASSWORD: messagebox.showerror("认证失败", "密码错误!") return # 如果是首次登录(只有管理员),显示特殊提示 is_only_admin = len(TRAINED_USERS) == 1 and "ADMIN" in TRAINED_USERS self.clear_screen() self.root.title("用户单管理") self.root.geometry("800x600") # 标题 title = tk.Label( self.root, text="用户单管理", font=("Arial", 24, "bold"), pady=20, bg=LOGIN_BG, fg="white" ) title.pack() # 如果是首次登录,显示提示信息 if is_only_admin: prompt = tk.Label( self.root, text="首次使用请添加至少一个普通用户账号", font=("黑体", 14), fg="red", pady=10 ) prompt.pack() # 框架容器 container = tk.Frame(self.root) container.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) # both指定容器在XY两个方向上填充父容器分配的空间, expand容器会随着父容器的扩大而扩展, pady设置容器的外间距(边距) # 创建Treeview显示用户(只显示工号) columns = ("工号",) # 单元素元组需要加逗号 self.user_tree = ttk.Treeview(container, columns=columns, show="headings", selectmode="browse") # 设置列标题 for col in columns: self.user_tree.heading(col, text=col) self.user_tree.column(col, width=100, anchor=tk.CENTER) # 添加滚动条 scrollbar = ttk.Scrollbar(container, orient=tk.VERTICAL, command=self.user_tree.yview) self.user_tree.configure(yscroll=scrollbar.set) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.user_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 刷新用户列表 self.refresh_user_list() # 按钮框架 btn_frame = tk.Frame(self.root) btn_frame.pack(pady=20) # 添加用户按钮 add_btn = tk.Button( btn_frame, text="添加用户", command=self.add_user, font=("黑体", 12), bg="#2ecc71", fg="white" ) add_btn.pack(side=tk.LEFT, padx=10) # 删除用户按钮 delete_btn = tk.Button( btn_frame, text="删除用户", command=self.delete_user, font=("黑体", 12), bg="#e74c3c", fg="white" ) delete_btn.pack(side=tk.LEFT, padx=10) # 导出用户单按钮 export_users_btn = tk.Button( btn_frame, text="导出文件", command=self.export_users_csv, font=("黑体", 12), bg=BUTTON_BG, fg="white" ) export_users_btn.pack(side=tk.LEFT, padx=10) # 返回按钮 back_btn = tk.Button( btn_frame, text="返回桌面", command=self.start_desktop_session, font=("黑体", 12), bg=BUTTON_BG, fg="white" ) back_btn.pack(side=tk.LEFT, padx=10) def refresh_user_list(self): """刷新用户列表显示(只显示工号)""" # 清除现有数据 for item in self.user_tree.get_children(): self.user_tree.delete(item) # 添加用户数据(只显示工号) for user_id in TRAINED_USERS.keys(): self.user_tree.insert("", tk.END, values=(user_id,)) def add_user(self): """添加新用户(只添加工号,密码为空)""" dialog = tk.Toplevel(self.root) dialog.title("添加新用户") dialog.geometry("200x150") dialog.transient(self.root) dialog.grab_set() # 窗口剧中设置 screen_width = dialog.winfo_screenwidth() screen_height = dialog.winfo_screenheight() x = (screen_width - 200) // 2 y = (screen_height - 150) // 2 dialog.geometry(f"200x150+{x}+{y}") # 只要求输入工号(不需要密码) tk.Label(dialog, text="工号:").pack(pady=(20, 0)) id_entry = tk.Entry(dialog, width=20) id_entry.pack() # tk.Label(dialog, text="密码:").pack(pady=(10, 0)) # password_entry = tk.Entry(dialog, show="*", width=20) # password_entry.pack() def save_new_user(): user_id = id_entry.get().strip() if not user_id: messagebox.showerror("错误", "工号不能为空", parent=dialog) return if user_id in TRAINED_USERS: messagebox.showerror("错误", "该工号已存在", parent=dialog) return # 添加到全局用户列表(密码初始化为空字串) TRAINED_USERS[user_id] = "" # 设置初始密码为空 save_user_file(TRAINED_USERS, USER_FILE) # 刷新显示 self.refresh_user_list() dialog.destroy() messagebox.showinfo("成功", f"用户 {user_id} 添加成功") tk.Button(dialog, text="保存", command=save_new_user).pack(pady=20) def delete_user(self): """删除选中用户""" selected = self.user_tree.selection() if not selected: messagebox.showerror("错误", "请先选择一个用户") return item = selected[0] values = self.user_tree.item(item, "values") user_id = values[0] if user_id == "ADMIN": messagebox.showerror("错误", "不能删除管理员账户") return if messagebox.askyesno("确认", f"确定要删除用户 {user_id} 吗?"): # 从全局用户列表中删除 if user_id in TRAINED_USERS: del TRAINED_USERS[user_id] save_user_file(TRAINED_USERS, USER_FILE) self.refresh_user_list() messagebox.showinfo("成功", f"用户 {user_id} 已删除") def update_usage_time(self): """更新使用时间显示""" if not self.is_locked: # 如果没有锁 usage_seconds = time.time() - self.login_time minutes, seconds = divmod(int(usage_seconds), 60) self.time_label.config(text=f"使用时间: {minutes:02d}:{seconds:02d}") self.root.after(1000, self.update_usage_time) # 1s后更新时间显示 def start_activity_monitoring(self): """启动鼠标键盘事件监听""" # 如果已有监听器在运行,先停止 if self.listening: self.stop_activity_monitoring() # 创建新的监听器实例 self.mouse_listener = mouse.Listener(on_move=self.activity_detected) self.keyboard_listener = keyboard.Listener(on_press=self.activity_detected) # 启动线程 mouse_thread = threading.Thread(target=self.mouse_listener.start) keyboard_thread = threading.Thread(target=self.keyboard_listener.start) mouse_thread.daemon = True keyboard_thread.daemon = True mouse_thread.start() keyboard_thread.start() self.listening = True def stop_activity_monitoring(self): """停止鼠标键盘事件监听""" if self.listening: if self.mouse_listener: self.mouse_listener.stop() if self.keyboard_listener: self.keyboard_listener.stop() self.mouse_listener = None self.keyboard_listener = None self.listening = False def activity_detected(self, *args): """检测到用户活动""" if not self.is_locked: # 系统未被锁定 self.last_activity = time.time() # 更新最后活动时间 # ⭐ 修改点1:确保在倒计时期间检测到活动时重新启动监听 if self.countdown_timer: # 如果倒计时正在进行 self.root.after_cancel(self.countdown_timer) self.countdown_timer = None # 重新启动桌面会话监听 self.stop_activity_monitoring() # 先停止当前监听 self.clear_screen() self.start_desktop_session() # 这会重新启动监听 def start_idle_monitor(self): """监控空闲状态""" if not self.is_locked: idle_time = time.time() - self.last_activity if idle_time > IDLE_THRESHOLD: # 空闲超时 self.start_lock_countdown() else: # 每秒检查一次 self.idle_timer = self.root.after(1000, self.start_idle_monitor) def start_lock_countdown(self): """开始锁屏倒计时""" # 关键修改:在倒计时开始前恢复全屏模式 self.root.overrideredirect(False) # 恢复窗口边框 self.root.attributes('-fullscreen', True) # 设置全屏 self.clear_screen() self.root.configure(bg=LOCK_SCREEN_BG) # self.root.attributes('-fullscreen', True) # 倒计时显示 self.countdown = COUNTDOWN_DURATION self.countdown_label = tk.Label( self.root, text=f"系统将在 {self.countdown} 秒后锁定...", font=("Arial", 36, "bold"), fg="red", bg=LOCK_SCREEN_BG ) self.countdown_label.place(relx=0.5, rely=0.4, anchor=tk.CENTER) # 提示信息 prompt = tk.Label( self.root, text="检测到系统空闲,移动鼠标或按键取消锁定", font=("Arial", 20), fg="#3498db", bg=LOCK_SCREEN_BG ) prompt.place(relx=0.5, rely=0.5, anchor=tk.CENTER) # 用户信息 user_info = tk.Label( self.root, text=f"当前用户: {self.current_user}", font=("Arial", 16), fg="white", bg=LOCK_SCREEN_BG ) user_info.place(relx=0.5, rely=0.6, anchor=tk.CENTER) # 开始倒计时 self.update_countdown() def update_countdown(self): """更新倒计时显示""" self.countdown -= 1 self.countdown_label.config(text=f"系统将在 {self.countdown} 秒后锁定...") if self.countdown <= 0: self.lock_system() else: # 每秒检查一次 self.countdown_timer = self.root.after(1000, self.update_countdown) def lock_system(self, manual=False): """锁定系统并保存使用记录""" logout_time = time.time() usage_seconds = logout_time - self.login_time # 格式化使用时间 minutes, seconds = divmod(int(usage_seconds), 60) usage_time = f"{minutes:02d}:{seconds:02d}" # 创建记录 record = { "user_id": self.current_user, "login_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.login_time)), "logout_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(logout_time)), "usage_duration": usage_time } # 加密保存记录 self.save_usage_record(record) # 停止监听计时器 if self.idle_timer: self.root.after_cancel(self.idle_timer) if self.countdown_timer: self.root.after_cancel(self.countdown_timer) self.stop_activity_monitoring() # 重置用户状态 self.current_user = None self.is_locked = True # 关键修改:在锁定时恢复全屏模式 self.root.overrideredirect(False) # 恢复窗口边框 self.root.attributes('-fullscreen', True) # 设置全屏 # 显示登录界面 self.show_login_screen() def save_usage_record(self, record): """加密保存使用记录""" # 生成或加载加密密钥 if not os.path.exists(KEY_FILE): # 检查密钥文件是否存在 key = Fernet.generate_key() # 若不存在(首次运行),Fernet.generate_key()生成一个新的对称加密密钥(256位),并以二进制模式写入文件 with open(KEY_FILE, "wb") as f: f.write(key) with open(KEY_FILE, "rb") as f: # 如果已存在,直接读取密钥内容 key = f.read() cipher = Fernet(key) # 创建Fernet加密器实例.Fernet是AES加密的封装库,提供简单易用的对称加密功能 # 读取已有记录,避免新记录覆盖旧数据 records = [] if os.path.exists(LOG_FILE): # 检查日志文件是否存在 with open(LOG_FILE, "rb") as f: # 如果存在,以二进制模式读取加密数据 encrypted_data = f.read() decrypted_data = cipher.decrypt(encrypted_data) # 使用cipher.decrypt()解密 records = json.loads(decrypted_data) # 通过json.loads()将JSON字串解析为Python列表 # 添加新记录 records.append(record) # 加密保存 encrypted_data = cipher.encrypt(json.dumps(records).encode()) # 将整个记录列表重新加密 with open(LOG_FILE, "wb") as f: f.write(encrypted_data) # 管理员管理使用记录⭐ def admin_view(self): """管理员查看使用记录""" password = simpledialog.askstring("❗", "请输入管理员密码:", show='*') if password == ADMIN_PASSWORD: records = self.get_usage_records() # 获取使用记录函数 self.display_records(records) # 显示使用记录函数 else: messagebox.showerror("认证失败", "密码错误!") # 密码框 def get_usage_records(self): """获取使用记录(解密)""" if not os.path.exists(KEY_FILE) or not os.path.exists(LOG_FILE): return [] try: with open(KEY_FILE, "rb") as f: key = f.read() cipher = Fernet(key) with open(LOG_FILE, "rb") as f: encrypted_data = f.read() decrypted_data = cipher.decrypt(encrypted_data) return json.loads(decrypted_data) except: return [] def display_records(self, records): """显示使用记录""" self.clear_screen() self.root.title("使用记录 - 管理员视图") self.root.geometry("1000x700") # 标题 title = tk.Label( self.root, text="电脑使用记录 - 安全报告", font=("Arial", 24, "bold"), pady=20, bg=LOGIN_BG, fg="white" ) title.pack() # 滚动文本框 text_area = scrolledtext.ScrolledText( self.root, wrap=tk.WORD, font=("Consolas", 12), width=120, height=30 ) text_area.pack(padx=20, pady=3, fill=tk.BOTH, expand=True) # 添加表头 header = "工号 登录时间 登出时间 使用时间\n" text_area.insert(tk.INSERT, header) text_area.insert(tk.INSERT, "-" * 65 + "\n") # 添加记录 for record in records: line = ( f"{record['user_id']:<8} " f"{record['login_time']:<20} " f"{record['logout_time']:<20} " f"{record['usage_duration']:<8}\n" # f"{record.get('lock_type', '自动'):<8}\n" ) text_area.insert(tk.INSERT, line) text_area.configure(state='disabled') # 设为只读 # 添加返回桌面按钮 btn_frame = tk.Frame(self.root) btn_frame.pack(pady=5) # pady参数在垂直方向(上下)添加10像素的填充,确保与其他界面元素有足够的间距 # 导出记录按钮 tk.Button( btn_frame, text="导出文件", command=lambda: self.export_records_csv(records), font=("黑体", 12), bg=BUTTON_BG, fg="white", width=15 ).pack(side=tk.LEFT, padx=10) # 返回按钮 tk.Button( btn_frame, text="返回桌面", command=self.start_desktop_session, font=("黑体", 12), bg=BUTTON_BG, fg="white", width=15 ).pack(side=tk.LEFT, padx=10) # # tk.Button( # btn_frame, # text="返回登录界面", # command=self.show_login_screen, # font=("黑体", 12), # bg=BUTTON_BG, # fg="white", # width=20 # ).pack(side=tk.LEFT, padx=10) # 将使用记录人员单导出 def export_records_csv(self, records): """导出使用记录为CSV文件""" try: # 获取当前时间作为文件 timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) filename = f"使用记录_{timestamp}.csv" with open(filename, 'w', newline='', encoding='utf-8') as file: writer = csv.writer(file) # 写入表头 writer.writerow(["工号", "登录时间", "登出时间", "使用时间"]) # 写入每条记录 for record in records: writer.writerow([ record['user_id'], record['login_time'], record['logout_time'], record['usage_duration'] ]) # ⭐导出为excel表格 # try: # # 获取当前时间作为文件 # timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) # filename = f"使用记录_{timestamp}.xlsx" # # # 创建DataFrame # data = { # "工号": [record['user_id'] for record in records], # "登录时间": [record['login_time'] for record in records], # "登出时间": [record['logout_time'] for record in records], # "使用时间": [record['usage_duration'] for record in records] # } # df = pd.DataFrame(data) # # # 导出到Excel # df.to_excel(filename, index=False, engine='openpyxl') # messagebox.showinfo("导出成功", f"使用记录已导出到: {os.path.abspath(filename)}") except Exception as e: messagebox.showerror("导出失败", f"导出使用记录时出错: {str(e)}") def export_users_csv(self): """导出用户单为CSV文件(解密后导出只包含工号)""" try: # 获取当前时间作为文件 timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) filename = f"用户单_{timestamp}.csv" with open(filename, 'w', newline='', encoding='utf-8') as file: writer = csv.writer(file) # 写入表头(只包含工号) writer.writerow(["工号"]) # 写入每条记录(只导出工号) for user_id in TRAINED_USERS.keys(): writer.writerow([user_id]) messagebox.showinfo("导出成功", f"用户单已解密并导出到: {os.path.abspath(filename)}") except Exception as e: messagebox.showerror("导出失败", f"导出用户单时出错: {str(e)}") """导出excel文件""" # try: # timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) # filename = f"用户单_{timestamp}.xlsx" # # # 创建dataframe # data = [] # for user_id, password in TRAINED_USERS.items(): # data.append([user_id, password]) # df = pd.DataFrame(data, columns=["工号", "密码"]) # # # 导出到excel # df.to_excel(filename, index=False, engine='openpyxl') def clear_screen(self): """清除当前屏幕所有内容""" for widget in self.root.winfo_children(): widget.destroy() def lock_on_close(self): """关闭窗口时锁定系统""" if self.current_user: self.lock_system(manual=True) self.root.destroy() # 启动系统 if __name__ == "__main__": app = SecureDesktopMonitor() # 绑定窗口关闭事件 app.root.protocol("WM_DELETE_WINDOW", app.lock_on_close)
最新发布
11-01
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值