Python 打开 SQLite3/MySQL/Oracle/SQLServer/PostgreSQL/等数据库示例,及使用Python自带GUI tkinter做的简单图形化操作数据库工具

【0】python 打开 SQLite3 数据库(Python 自带 SQLite3 模块,无需安装)

## SQLite3 数据库信息查询
表名及建表语句等: SELECT * FROM sqlite_master
数据库内所有表名: SELECT name FROM sqlite_master
某表的建表语句等: SELECT sql FROM sqlite_master where name='表名'
表字段属性信息:   PRAGMA table_info(表名)

示例:

# -*- coding: utf8 -*-
import sqlite3  # Python 自带模块

FILE_SQLite3_PATH = 'E:\\TEST\\test.db3'    # 数据库文件路径

数据库连接对象 = sqlite3.connect(FILE_SQLite3_PATH)     # 尝试打开数据库文件(文件不存在会自动新建)
for 数据库连接对象_方法 in dir(数据库连接对象):         # 查看连接对象可用方法
    print(数据库连接对象_方法)
'''
常用方法
close           # 关闭连接
commit          # 提交修改
cursor          # 创建游标
'''

游标对象 = 数据库连接对象.cursor()    # 创建一个游标
for 游标对象_方法 in dir(游标对象):   # 查看游标对象可用方法
    print(游标对象_方法)
'''
常用方法
close           # 关闭游标
description     # 获取字段信息
execute         # 执行一条SQL语句
executemany     # 执行批量SQL语句
executescript   # 执行SQL脚本
fetchall        # 获取全部查询结果
fetchmany       # 获取部分查询结果
fetchone        # 获取一条查询结果
rowcount        # 受影响行数
'''
游标对象.close()
数据库连接对象.close()


## 执行查询SQL语句示例
SQL_CMD = 'SELECT SQL FROM sqlite_master'  ## SQLite3查表命令,返回数据结构 [('sqlite_sequence',), ('t0',), ('t1',)]
数据库连接对象 = sqlite3.connect(FILE_SQLite3_PATH)
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
查询结果集 = 游标对象.fetchall()
字段信息 = 游标对象.description
print(f"查询结果集={查询结果集}")
print(f"字段信息 = {字段信息}")
for 记录 in 查询结果集:
    print(记录)
游标对象.close()
数据库连接对象.close()


## 修改数据库语句示例(执行一条SQL)
SQL_CMD = "INSERT INTO TABLE_A (A,B,C) VALUES (1, '1', '1')"
数据库连接对象 = sqlite3.connect(FILE_SQLite3_PATH)
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
数据库连接对象.commit()        # 提交更改
游标对象.close()
数据库连接对象.close()


## 修改数据库语句示例(批量执行)
SQL_TMPL = f'INSERT INTO 表名 (时间,地点,事件) VALUES (?,?,?)'  # 批量插入模板语句
LL_DATA = [['09:00','A',1],['10:00','B',2]]                     # 批量插入数据列表的列表
数据库连接对象 = sqlite3.connect(FILE_SQLite3_PATH)
游标对象 = 数据库连接对象.cursor()
游标对象.executemany(SQL_TMPL, LL_DATA)
数据库连接对象.commit()        # 提交更改
游标对象.close()
数据库连接对象.close()


## 执行数据库脚本
SQL_CMD = '--脚本文本内容...'
数据库连接对象 = sqlite3.connect(FILE_SQLite3_PATH)
游标对象 = 数据库连接对象.cursor()
游标对象.executescript(SQL_CMD)
数据库连接对象.commit()        # 提交更改
游标对象.close()
数据库连接对象.close()

【1】python 打开 MySQL 数据库

需安装第三方模块 pip3 install PyMySQL  或 pip install mysqlclient

## MySQL 数据库信息获取
#查所有数据库:     SHOW DATABASES;
#查数据库内数据表: SHOW TABLES FROM 库名;
#某表的建表语句等: SHOW CREATE TABLE 库名.表名;
#表字段属性信息:   SELECT * FROM information_schema.columns WHERE TABLE_SCHEMA='库名' AND TABLE_NAME='表名';
#查引擎:           SELECT * FROM information_schema.tables  WHERE TABLE_SCHEMA='库名' AND TABLE_NAME='表名';

使用示例:

import pymysql

登录地址 = '192.168.62.62'
登录帐号 = 'root'
登录密码 = 'r@123'
登录库名 = ''
服务端口 = 3306
字符集 = 'utf8'

数据库连接对象 = pymysql.connect(host=登录地址, user=登录帐号, password=登录密码, database=登录库名, port=服务端口, charset=字符集)
for 数据库连接对象_方法 in dir(数据库连接对象):         # 查看连接对象可用方法
    print(数据库连接对象_方法)
'''
常用部分:
begin               # 开启事务
close               # 关闭连接
commit              # 提交修改
rollback            # 回退操作
'''

游标对象 = 数据库连接对象.cursor()
for 游标对象_方法 in dir(游标对象):   # 查看游标对象可用方法
    print(游标对象_方法)
'''
常用部分:
close       # 关闭游标
description # 获取字段信息
execute     # 执行一条SQL语句
executemany # 执行批量SQL语句
fetchall    # 获取全部查询结果
fetchmany   # 获取部分查询结果
fetchone    # 获取一条查询结果
rowcount    # 受影响行数
rownumber   # 本次获取记录数量
'''

游标对象.close()
数据库连接对象.close()


## SQL查询语句示例
SQL_CMD = 'SELECT * FROM 库名.表名'
数据库连接对象 = pymysql.connect(host=登录地址, user=登录帐号, password=登录密码, database=登录库名, port=服务端口, charset=字符集)
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
查询结果集 = 游标对象.fetchall()    # 取全部记录
#查询结果集 = 游标对象.fetchone()    # 取1条记录
#查询结果集 = 游标对象.fetchmany(2)   # 取2条记录
字段信息 = 游标对象.description
print(f"字段信息 = {字段信息}")
print(f"查询结果集={查询结果集}")
print("受影响行数(记录总数)", 游标对象.rowcount)
print("返回的行数(获取行数)", 游标对象.rownumber)
print("格式化打印")
print('字段名', '\t'.join([str(i[0]) for i in 字段信息]))
for 记录 in 查询结果集:
    print('记录值', '\t'.join([str(i) for i in 记录]))
游标对象.close()
数据库连接对象.close()


## SQL修改语句示例(执行一条SQL)
SQL_CMD = "UPDATE 库名.表名 SET 字符='X' WHERE id1=6"
数据库连接对象 = pymysql.connect(host=登录地址, user=登录帐号, password=登录密码, database=登录库名, port=服务端口, charset=字符集)
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
print('受影响行数', 游标对象.rowcount)
数据库连接对象.commit()        # 提交更改
游标对象.close()
数据库连接对象.close()


## SQL修改语句示例(批量执行)
SQL_TMPL = "INSERT INTO 库名.表名 (id1,id2,字符,日期,时间) VALUES (%s,%s,%s,%s,%s)"  # 批量插入模板语句
LL_DATA = [[6,6,6,6,6],[7,7,7,7,7]]                     # 批量插入数据列表的列表
数据库连接对象 = pymysql.connect(host=登录地址, user=登录帐号, password=登录密码, database=登录库名, port=服务端口, charset=字符集)
游标对象 = 数据库连接对象.cursor()
游标对象.executemany(SQL_TMPL, LL_DATA)
print("受影响行数", 游标对象.rowcount)
数据库连接对象.commit()        # 提交更改
游标对象.close()
数据库连接对象.close()

【2】python 打开 Oracle 数据库

安装库 pip install cx_Oracle

根据服务器Oracle版本在官网下载 对应客户端基本包:
instantclient-basic-win64-10.2.0.5.zip
instantclient-basic-win-x86-64-11.1.0.7.0.zip
instantclient-basic-windows.x64-11.2.0.4.0.zip
instantclient-basic-windows.x64-12.1.0.2.0.zip
instantclient-basic-windows.x64-12.2.0.1.0.zip
instantclient-basic-windows.x64-18.5.0.0.0dbru.zip
instantclient-basic-windows.x64-19.16.0.0.0dbru.zip
instantclient-basic-windows.x64-21.6.0.0.0dbru.zip

我的服务器是11.2版本用:instantclient-basic-windows.x64-11.2.0.4.0.zip

解压后记住路径,设置cx_Oracle调用客户端lib的路径:

使用示例:

import cx_Oracle

# 解压后指定路径
cx_Oracle.init_oracle_client(lib_dir="D:\\instantclient-basic-windows.x64-11.2.0.4.0\\instantclient_11_2")

登录帐号 = 'user1'
登录密码 = 'pwd123'
登录地址 = '192.168.62.62'
服务端口 = '1521'
实例 = 'orcl'

数据库连接对象 = cx_Oracle.connect(登录帐号, 登录密码, f'{登录地址}:{服务端口}/{实例}')
#for 数据库连接对象_方法 in dir(数据库连接对象):         # 查看连接对象可用方法
#    print(数据库连接对象_方法)
print("登录用户名(相当于库名)", 数据库连接对象.username)
print("版本", 数据库连接对象.version)

游标对象 = 数据库连接对象.cursor()
#for 游标对象_方法 in dir(游标对象):   # 查看游标对象可用方法
#    print(游标对象_方法)

游标对象.close()
数据库连接对象.close()


## SQL查询语句示例
SQL_CMD = ''
数据库连接对象 = cx_Oracle.connect(登录帐号, 登录密码, f'{登录地址}:{服务端口}/{实例}')
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
查询结果集 = 游标对象.fetchall()    # 取全部记录
#查询结果集 = 游标对象.fetchone()    # 取1条记录
#查询结果集 = 游标对象.fetchmany(2)   # 取2条记录
字段信息 = 游标对象.description
print(f"字段信息 = {字段信息}")
print(f"查询结果集={查询结果集}")
print("受影响行数(获取行数)", 游标对象.rowcount)
print("格式化打印")
print('字段名', '\t'.join([str(i[0]) for i in 字段信息]))
for 记录 in 查询结果集:
    print('记录值', '\t'.join([str(i) for i in 记录]))
游标对象.close()
数据库连接对象.close()



## SQL修改语句示例(执行一条SQL)
SQL_CMD = ""
数据库连接对象 = cx_Oracle.connect(登录帐号, 登录密码, f'{登录地址}:{服务端口}/{实例}')
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
print('受影响行数', 游标对象.rowcount)
数据库连接对象.commit()        # 提交更改
游标对象.close()
数据库连接对象.close()


## Oracle 数据库信息获取
#查所有数据库:     
#查数据库内数据表: SELECT table_name FROM user_tables
#某表的建表语句等: 未知
#表字段属性信息:   SELECT * FROM user_TAB_COLUMNS WHERE TABLE_NAME='表名'
#表主键:           SELECT * FROM user_ind_columns where TABLE_NAME='表名'

【3】python 打开 MSSQL(SQL Server) 数据库

pip install pymssql

## MSSQL 库名表名格式:库名..表名
#查库     SELECT name FROM master..sysdatabases
#查表     SELECT name FROM 库名..sysobjects WHERE xtype='U'
#查字段   select * from information_schema.columns where TABLE_CATALOG = '库名' AND TABLE_NAME ='表名'
#查主键   select table_name, column_name as '主键' from information_schema.key_column_usage where table_name = '表名'

使用示例:

import pymssql

登录帐号 = 'sa'
登录密码 = '111111'
登录地址 = '192.168.62.62'
服务端口 = 1433
登录库名 = ''
字符集 = 'UTF-8'

数据库连接对象 = pymssql.connect(server=登录地址, user=登录帐号, password=登录密码, database=登录库名, port=服务端口, charset=字符集)
#for 数据库连接对象_方法 in dir(数据库连接对象):         # 查看连接对象可用方法
#    print(数据库连接对象_方法)

游标对象 = 数据库连接对象.cursor()
#for 游标对象_方法 in dir(游标对象):   # 查看游标对象可用方法
#    print(游标对象_方法)

游标对象.close()
数据库连接对象.close()


## SQL查询语句示例
SQL_CMD = 'SELECT * FROM master..表名'
数据库连接对象 = pymssql.connect(server=登录地址, user=登录帐号, password=登录密码, database=登录库名, port=服务端口, charset=字符集)
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
查询结果集 = 游标对象.fetchall()    # 取全部记录
#查询结果集 = 游标对象.fetchone()    # 取1条记录
#查询结果集 = 游标对象.fetchmany(2)   # 取2条记录
字段信息 = 游标对象.description
print(f"字段信息 = {字段信息}")
print(f"查询结果集={查询结果集}")
print("受影响行数(记录总数)", 游标对象.rowcount)
print("返回的行数(获取行数)", 游标对象.rownumber)
print("格式化打印")
print('字段名', '\t'.join([str(i[0]) for i in 字段信息]))
for 记录 in 查询结果集:
    print('记录值', '\t'.join([str(i) for i in 记录]))
游标对象.close()
数据库连接对象.close()



## SQL修改语句示例(执行一条SQL)
SQL_CMD = ""
数据库连接对象 = pymssql.connect(server=登录地址, user=登录帐号, password=登录密码, database=登录库名, port=服务端口, charset=字符集)
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
print('受影响行数', 游标对象.rowcount)
数据库连接对象.commit()        # 提交更改
游标对象.close()
数据库连接对象.close()

【4】python 打开 PostgreSQL 数据库

pip install psycopg2

#查库     SELECT schema_name FROM information_schema.schemata
#查表     SELECT table_name FROM information_schema.tables WHERE table_schema = '库名'
#查字段   SELECT * FROM information_schema.columns WHERE table_schema='库名' AND table_name='表名'
#查主键   SELECT pg_attribute.attname FROM pg_constraint inner join pg_class on pg_constraint.conrelid = pg_class.oid inner join pg_attribute on pg_attribute.attrelid = pg_class.oid and  pg_attribute.attnum = pg_constraint.conkey[1] inner join pg_type on pg_type.oid = pg_attribute.atttypid where pg_class.relname = '表名' and pg_constraint.contype='p'
使用示例:

# -*- coding: utf8 -*-
import psycopg2

登录帐号 = 'postgres'
登录密码 = '123456'
登录地址 = '192.168.62.62'
服务端口 = '5432'
实例 = ''

#登录帐号 = 'test1'
#登录密码 = '123456'
#登录地址 = '192.168.62.62'
#服务端口 = '5432'
#实例 = 'test_db'          # 普通用户登录必须指定正确库名,默认使用用户名做库名


数据库连接对象 = psycopg2.connect(database=实例, user=登录帐号, password=登录密码, host=登录地址, port=服务端口)
#for 数据库连接对象_方法 in dir(数据库连接对象):         # 查看连接对象可用方法
#    print(数据库连接对象_方法)

游标对象 = 数据库连接对象.cursor()
#for 游标对象_方法 in dir(游标对象):   # 查看游标对象可用方法
#    print(游标对象_方法)

游标对象.close()
数据库连接对象.close()


## SQL查询语句示例
SQL_CMD = "SELECT * FROM 库名.表名"
数据库连接对象 = psycopg2.connect(database=实例, user=登录帐号, password=登录密码, host=登录地址, port=服务端口)
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
查询结果集 = 游标对象.fetchall()    # 取全部记录
#查询结果集 = 游标对象.fetchone()    # 取1条记录
#查询结果集 = 游标对象.fetchmany(2)   # 取2条记录
字段信息 = 游标对象.description
print(f"字段信息 = {字段信息}")
print(f"查询结果集={查询结果集}")
print("受影响行数(记录总数)", 游标对象.rowcount)
print("返回的行数(获取行数)", 游标对象.rownumber)
print("格式化打印")
print('字段名', '\t'.join([str(i[0]) for i in 字段信息]))
for 记录 in 查询结果集:
    print('记录值', '\t'.join([str(i) for i in 记录]))
游标对象.close()
数据库连接对象.close()


## SQL修改语句示例(执行一条SQL)
SQL_CMD = "UPDATE 库名.表名 SET 字段名='1' WHERE id<4"
数据库连接对象 = psycopg2.connect(database=实例, user=登录帐号, password=登录密码, host=登录地址, port=服务端口)
游标对象 = 数据库连接对象.cursor()
游标对象.execute(SQL_CMD)
print('受影响行数', 游标对象.rowcount)
数据库连接对象.commit()        # 提交更改
游标对象.close()
数据库连接对象.close()

把上面代码简单放在一起,再加点python自带的图形化tkinter模块,一个简单的图形化操作多种数据库的工具就完成了。

#_*_ coding:utf8 _*_
## Python3 GUI tkinter 数据库图形化操作工具(Python 自带 GUI tkinter 功能使用示例)
## 尝试汇总 SQLite3/MySQL/Oracle/SQLServer/PostgreSQL/...
## V2.0.14 修复一些大小BUG,修修补补继续将就着用吧~
## BUG 修改数据时,如果主键有值和自定义空值表示相同,则无法修改,需设置新字符串表示空值后才能修改
## BUG 对乱操作的限制没有做...
## 施工中:tree横滚动条(SOS求助:横滚动条咋不生效啊)
## 施工中:新建查询窗口
## 注意:程序日志文件 PY3_DB_GUI.log 中会有部分数据库信息及数据,及时删除日志文件或禁止日志(或设置最高级别CRITICAL就不会记录了)

import os,time,re
from tkinter import *
from tkinter import filedialog  # 选择文件用
from tkinter import ttk         # 下拉菜单控件在ttk中
import tkinter.messagebox       # 弹出提示对话框
import tkinter.simpledialog     # 弹出对话框,获取用户输入
import csv                      # CSV文件操作模块,用于导出数据

## SQLite3
import sqlite3      # Python 自带 SQLite3 模块

## MySQL
try:
    #import MySQLdb             # MySQL 第三方模块,使用 pip install mysqlclient 安装
    import pymysql              # MySQL 第三方模块,使用 pip3 install PyMySQL 安装
except Exception as e:
    print(f"import pymysql 失败\n{e}\n要使用MySQL数据库先安装pymysql模块(pip install PyMySQL)")

## Oracle
try:
    import cx_Oracle
    # 官网下载 对应客户端基本包 instantclient-basic-windows.x64-11.2.0.4.0.zip
    # https://www.oracle.com/database/technologies/instant-client/winx64-64-downloads.html
    # 解压后指定路径
    cx_Oracle.init_oracle_client(lib_dir="D:\\instantclient-basic-windows.x64-11.2.0.4.0\\instantclient_11_2")
except Exception as e:
    print(f"import cx_Oracle 失败\n{e}\n要使用Oracle数据库先安装cx_Oracle模块(pip install cx_Oracle)")

## MSSQL/SQLServer
try:
    import pymssql              # MSSQL 第三方模块,使用 pip install pymssql 安装
except Exception as e:
    print(f"import pymssql 失败\n{e}\n要使用MSSQL数据库先安装pymssql模块(pip install pymssql)")

## PostgreSQL
try:
    import psycopg2             # PostgreSQL 第三方模块,使用 pip install psycopg2 安装
except Exception as e:
    print(f"import psycopg2 失败\n{e}\n要使用PostgreSQL数据库先安装psycopg2模块(pip install psycopg2)")


import logging                                                             # 日志模块
Log = logging.getLogger('__name__')                                        # 获取实例
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')   # 指定logger输出格式
file_handler = logging.FileHandler('PY3_DB_GUI.log')                       # 日志文件路径
file_handler.setFormatter(formatter)                                       # 可以通过setFormatter指定输出格式
Log.addHandler(file_handler)                                               # 为logger添加的日志处理器

## 同时在终端显示
console = logging.StreamHandler()
console.setLevel(logging.DEBUG) # 显示所有操作过程信息
Log.addHandler(console)

# 设置记录的日志级别
Log.setLevel(logging.DEBUG)     # 记录所有操作过程信息
#Log.setLevel(logging.INFO)     # 记录重点操作过程信息
#Log.setLevel(logging.WARNING)  # 记录改动数据库的信息
#Log.setLevel(logging.ERROR)    # 记录已知错误信息
#Log.setLevel(logging.CRITICAL) # 严重错误


## 通过外置字体计算字符串像素宽返回Entry标准宽,PIL模块Python3安装 pip install pillow
try:
    from PIL import ImageFont
    字体文件路径 = 'C:\\Windows\\Fonts\\Arial.ttf'    # Win10 字体文件位置
    #字体文件路径 = 'Arial.ttf'                       # 复制到此脚本同目录
    font_Arial_8 = ImageFont.truetype(字体文件路径, 8, encoding='utf-8')
except Exception as e:
    Log.info(f"{e} 加载Windows字体Arial.ttf失败,无法精确计算字符宽度")
    加载外置字体失败 = 0
else:
    加载外置字体失败 = 1

def 计算字符串像素宽返回Entry标准宽(字符串, 外置字体=加载外置字体失败):
    if 字符串 == None:         # 空值
        return(4)              # 返回宽度4
    if 外置字体 == 1:
        字符串_w, 字符串_h = font_Arial_8.getsize(字符串)
        return(字符串_w//4+2+1)  # 像素级精确控制(中文可能还显示不下,再多加1)
    else:
        return(len(字符串)*2)  # 统一给2倍的量

def DEV_SQLite3_OPEN(DB_File):                       # 打开SQLite3数据库文件,不存在会自动创建
    try:
        conn = sqlite3.connect(DB_File)     # 尝试打开数据库文件
    except Exception as e:
        E = '打开数据库文件失败 ' + str(e)
        return(1, E)                        # 返回错误代码1和失败原因
    else:
        return(0, conn)                     # 返回成功代码0和数据库连接对象
def DEV_MySQL_OPEN(登录地址, 登录帐号, 登录密码, 服务端口, 登录库名, 字符集):   # 连接MySQL数据库
    try:
        #数据库连接对象 = MySQLdb.connect(登录地址, 登录帐号, 登录密码, 登录库名, 服务端口, 字符集)
        数据库连接对象 = pymysql.connect(host=登录地址, user=登录帐号, password=登录密码, database=登录库名, port=服务端口, charset=字符集)
    except Exception as e:
        E = '连接数据库失败 ' + str(e)
        return(1, E)                      # 返回错误代码1和失败原因
    else:
        return(0, 数据库连接对象)         # 返回成功代码0和数据库连接对象
def DEV_MSSQL_OPEN(登录地址, 登录帐号, 登录密码, 服务端口, 登录库名, 字符集):   # 连接MSSQL数据库
    try:
        数据库连接对象 = pymssql.connect(host=登录地址, user=登录帐号, password=登录密码, database=登录库名, port=服务端口, charset=字符集)
    except Exception as e:
        E = '连接数据库失败 ' + str(e)
        return(1, E)                      # 返回错误代码1和失败原因
    else:
        return(0, 数据库连接对象)         # 返回成功代码0和数据库连接对象
def DEV_PostgreSQL_OPEN(登录帐号, 登录密码, 登录地址, 服务端口, 登录库名):      # 连接PostgreSQL数据库
    try:
        数据库连接对象 = psycopg2.connect(database=登录库名, user=登录帐号, password=登录密码, host=登录地址, port=服务端口)
    except Exception as e:
        E = '连接数据库失败 ' + str(e)
        return(1, E)                      # 返回错误代码1和失败原因
    else:
        return(0, 数据库连接对象)         # 返回成功代码0和数据库连接对象
def DEV_ORACLE_OPEN(USER, PASS, HOST, PORT, 实例):   # 连接Oracle数据库
    try:
        数据库连接对象 = cx_Oracle.connect(USER, PASS, f'{HOST}:{PORT}/{实例}')
    except Exception as e:
        E = '连接数据库失败 ' + str(e)
        return(1, E)                      # 返回错误代码1和失败原因
    else:
        return(0, 数据库连接对象)         # 返回成功代码0和数据库连接对象

def DEF_SQL_查询和返回(SQL_CMD):                     # 执行SQL查询语句(DQL - 数据查询语言 SELECT 或 SQLite3 pragma 命令),返回执行状态和执行结果(数据列表)
    数据库连接对象 = DB_INFO['数据库连接对象']
    try:
        游标对象 = 数据库连接对象.cursor()     # 创建一个游标
    except Exception as e:
        CRITICAL = f'创建游标失败 {e}'
        Log.critical(CRITICAL)
        return(1, CRITICAL)
    else:
        try:
            游标对象.execute(SQL_CMD)
        except Exception as e:
            ERROR = f'执行SQL语句 {SQL_CMD} 失败 {e}'
            Log.error(ERROR)
            TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
            Log.debug(f'游标对象={游标对象} 保持游标')
            return(1, ERROR)
        else:
            全部记录 = 游标对象.fetchall()
            游标对象.close()
            DB_INFO['数据库游标对象'] = ''
            SV_数据库游标对象.set('')
            Log.debug(f'游标对象={游标对象} 关闭游标')
            INFO = f'执行SQL语句 {SQL_CMD} 成功 返回 {len(全部记录)} 条记录'
            TEXT_数据库操作日志.insert(0.0, INFO+'\n', 'tag_i')
            return(0, 全部记录)
def DEF_SQL_查询和返回_数据列表_字段列表(SQL_CMD):   # 执行SQL查询语句,返回执行状态和执行结果(数据列表,字段列表)导出使用
    数据库连接对象 = DB_INFO['数据库连接对象']
    try:
        游标对象 = 数据库连接对象.cursor()
    except Exception as e:
        CRITICAL = f'创建游标失败 {e}'
        Log.critical(CRITICAL)
        return(1, CRITICAL)
    else:
        try:
            游标对象.execute(SQL_CMD)
        except Exception as e:
            游标对象.close()
            ERROR = f'执行SQL语句 {SQL_CMD} 失败 {e}'
            Log.error(ERROR)
            TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
            Log.debug(f'游标对象={游标对象} 关闭游标')
            return(1, ERROR)
        else:
            全部记录 = 游标对象.fetchall()                       # 获取全部查询数据记录
            游标对象_字段名列表 = 游标对象.description           # 获取查询结果的字段信息
            字段名列表 = [i[0] for i in 游标对象_字段名列表]     # 整理成字段名列表
            游标对象.close()
            Log.debug(f'游标对象={游标对象} 关闭游标')
            INFO = f'执行SQL语句 {SQL_CMD} 成功 返回 {len(全部记录)} 条记录'
            TEXT_数据库操作日志.insert(0.0, INFO+'\n', 'tag_i')
            return(0, 全部记录, 字段名列表)

def DEF_查表字段属性返回字段名列表及主键列表():
    L_字段名 = []
    L_主键 = []
    数据库名 = SV_数据库树_选中库名.get()
    数据表名 = SV_数据库树_选中表名.get()
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        SQL_CMD = f'PRAGMA table_info({数据表名})'  # 获取字段属性命令
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            字段信息 = R[1]             # 示例 [(0, 'ID', 'INTEGER', 1, None, 1), (1, 'A', 'TEXT', 0, None, 0)]
            Log.debug(f'{当前数据库类型} 数据表名={数据表名} 字段信息={字段信息}')
            L_字段名 = [i[1] for i in 字段信息]           # 字段名在第2列
            L_主键 = [i[1] for i in 字段信息 if i[5]!=0]  # 第6列为非0表示是主键
        else:
            TEXT_数据库操作日志.insert(0.0, R[1]+'\n', 'tag_e')
    elif 当前数据库类型 == 'MySQL':
        SQL_CMD = f"SELECT COLUMN_NAME,COLUMN_KEY FROM information_schema.columns WHERE TABLE_SCHEMA='{数据库名}' AND TABLE_NAME='{数据表名}'"
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            字段信息 = R[1]
            Log.debug(f'{当前数据库类型} 数据表名={数据表名} 字段信息={字段信息}')
            L_字段名 = [i[0] for i in 字段信息]                 # 字段名在第1列
            L_主键 = [i[0] for i in 字段信息 if i[1] == 'PRI']  # 第2列为PRI表示是主键
        else:
            TEXT_数据库操作日志.insert(0.0, R[1]+'\n', 'tag_e')
    elif 当前数据库类型 == 'Oracle':
        SQL_CMD = f"SELECT COLUMN_NAME FROM user_TAB_COLUMNS WHERE TABLE_NAME='{数据表名}'"
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            字段信息 = R[1]
            Log.debug(f'{当前数据库类型} 数据表名={数据表名} 字段信息={字段信息}')
            L_字段名 = [i[0] for i in 字段信息]                 # 字段名在第1列
            SQL_CMD2 = f"SELECT COLUMN_NAME FROM user_ind_columns WHERE TABLE_NAME='{数据表名}'"
            RR = DEF_SQL_查询和返回(SQL_CMD2)
            if RR[0] == 0:
                主键信息 = RR[1]
                L_主键 = [i[0] for i in 主键信息]
            else:
                TEXT_数据库操作日志.insert(0.0, RR[1]+'\n', 'tag_e')
        else:
            TEXT_数据库操作日志.insert(0.0, R[1]+'\n', 'tag_e')
    elif 当前数据库类型 == 'MSSQL':
        SQL_CMD = f"SELECT COLUMN_NAME FROM information_schema.columns WHERE TABLE_CATALOG='{数据库名}' AND TABLE_NAME='{数据表名}'"
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            字段信息 = R[1]
            Log.debug(f'{当前数据库类型} 数据库名={数据库名} 数据表名={数据表名} 字段信息={字段信息}')
            L_字段名 = [i[0] for i in 字段信息]                 # 字段名在第1列
            SQL_CMD2 = f"SELECT COLUMN_NAME FROM information_schema.key_column_usage WHERE TABLE_NAME='{数据表名}'"
            RR = DEF_SQL_查询和返回(SQL_CMD2)
            if RR[0] == 0:
                主键信息 = RR[1]
                L_主键 = [i[0] for i in 主键信息]
            else:
                TEXT_数据库操作日志.insert(0.0, RR[1]+'\n', 'tag_e')
        else:
            TEXT_数据库操作日志.insert(0.0, R[1]+'\n', 'tag_e')
    elif 当前数据库类型 == 'PostgreSQL':
        SQL_CMD = f"SELECT column_name FROM information_schema.columns WHERE table_schema='{数据库名}' AND table_name='{数据表名}'"
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            字段信息 = R[1]
            Log.debug(f'{当前数据库类型} 数据表名={数据表名} 字段信息={字段信息}')
            L_字段名 = [i[0] for i in 字段信息]                 # 字段名在第1列
            SQL_CMD2 = f"SELECT pg_attribute.attname FROM pg_constraint inner join pg_class on pg_constraint.conrelid = pg_class.oid inner join pg_attribute on pg_attribute.attrelid = pg_class.oid and  pg_attribute.attnum = pg_constraint.conkey[1] inner join pg_type on pg_type.oid = pg_attribute.atttypid where pg_class.relname = '{数据表名}' and pg_constraint.contype='p'"
            RR = DEF_SQL_查询和返回(SQL_CMD2)
            if RR[0] == 0:
                主键信息 = RR[1]
                L_主键 = [i[0] for i in 主键信息]
            else:
                TEXT_数据库操作日志.insert(0.0, RR[1]+'\n', 'tag_e')
        else:
            TEXT_数据库操作日志.insert(0.0, R[1]+'\n', 'tag_e')
    else:
        ERROR = f'未知{当前数据库类型}类型数据库的查询表字段主键的SQL语句'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    return(L_字段名,L_主键)
def 判断主键是否可用():             # 查找主键,为避免错误修改,数据表必须有主键,根据主键修改对应行数据,没有主键的表只能通过命令框手写语句修改
    当前字段列表 = DB_INFO['字段名列表']
    L_字段名, L_主键 = DEF_查表字段属性返回字段名列表及主键列表()
    if L_主键 == []:
        Log.debug('没有主键')
        return(1, f'没有主键,为防止自动操作出错,请通过手工执行SQL语句操作')
    else:
        P_未出现的主键 = set(L_主键) - set(当前字段列表)  # 集合计算,找出未出现在显示字段的主键
        if len(P_未出现的主键) != 0:
            Log.debug(f'L_主键={L_主键} 当前字段列表={当前字段列表} P_未出现的主键={P_未出现的主键}')
            return(1, f'L_主键={L_主键} 未显示的主键={P_未出现的主键},为防止自动操作出错,请通过手工执行SQL语句操作')
        else:
            L_主键信息 = []
            for 主键名 in L_主键:
                列号 = 当前字段列表.index(主键名)
                主键信息 = (列号,主键名)
                L_主键信息.append(主键信息)
            Log.debug(f'L_主键={L_主键} 当前字段列表={当前字段列表} L_主键信息={L_主键信息} 主键全部在场')
            return(0, L_主键信息)
def DEF_SQL_查询和显示(SQL_CMD, 显示开始行定位=0):  # 执行SQL查询语句,直接显示在界面,不返回。参数:显示开始行定位,从指定行后开始显示,默认值为0,表示从头全部显示
    ## 是否应该关一下不用的游标对象 if DB_INFO['数据库游标对象'] != ''
    数据库连接对象 = DB_INFO['数据库连接对象']
    try:
        游标对象 = 数据库连接对象.cursor()                       # 创建一个游标
    except Exception as e:
        CRITICAL = '创建游标失败' + str(e)
        Log.critical(CRITICAL)
        tkinter.messagebox.showerror(title='CRITICAL', message=CRITICAL)
    else:
        try:
            游标对象.execute(SQL_CMD)
        except Exception as e:
            ERROR = f'执行SQL语句 {SQL_CMD} 失败 {e}'
            Log.error(ERROR)
            TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
        else:
            DEBUG = f'执行SQL语句 {SQL_CMD} 成功'
            Log.debug(DEBUG)
            TEXT_数据库操作日志.insert(0.0, DEBUG+'\n', 'tag_i')
            游标对象_字段名列表 = 游标对象.description           # 获取字段信息
            字段名列表 = [i[0] for i in 游标对象_字段名列表]
            DB_INFO['字段名列表'] = 字段名列表                   # 保存字段名查询结果
            SV_查询字段列表.set(str(字段名列表))                 # 显示字段名查询结果
            DB_INFO['最后查询语句'] = SQL_CMD                    # 保存成功执行的SQL查询语句
            SV_最后查询语句.set(SQL_CMD)                         # 显示成功执行的SQL查询语句
            
            if 显示开始行定位 > 0:
                丢弃记录 = 游标对象.fetchmany(显示开始行定位)    # 先从SQL查询结果中取编辑位置前面的内容部分,丢弃
                IV_已显示记录数.set(显示开始行定位)
            else:
                IV_已显示记录数.set(0)                           ### 用于修改数据库后,再次显示在修改位置
                IV_上次分页行数.set(0)                           ### 用于修改数据库后,再次显示在修改位置

            ## 分页控制
            ## 游标对象.fetchmany(<=0) 和 游标对象.fetchall() 效果一样,为读取全部数据
            分页限制行数 = IV_设置分页行数.get()
            if 分页限制行数 > 0:                                 # 分页限制行数 > 0 读取部分记录,可分页显示
                部分记录 = 游标对象.fetchmany(分页限制行数)      # 从SQL查询结果中取指定行数的记录,如果查询结果为空则返回空列表
                实际读取记录行数 = len(部分记录)
                IV_已显示记录数.set(IV_已显示记录数.get() + 实际读取记录行数)   ### 用于修改数据库后,再次显示在修改位置
                IV_上次分页行数.set(实际读取记录行数)                           ### 用于修改数据库后,再次显示在修改位置
                if 部分记录 != []:
                    字段和数据的存储和展示(字段名列表, 部分记录) # 在显编框展示结果,保存结果到全局变量,可以进行修改操作
                    if 实际读取记录行数 < 分页限制行数:          # 已经全部显示,无法分页
                        游标对象.close()                         # 关闭游标对象
                        DB_INFO['数据库游标对象'] = ''           # 清空数据库游标对象
                        SV_数据库游标对象.set('')
                        按钮_显编框下一页['state'] = 'disabled'  # 禁止下一页按钮(第一次显示就全部显示完整,不需要下一页按钮)
                        DEBUG = f"游标对象={游标对象} 关闭游标(数据<分页第一页) 设置 DB_INFO['数据库游标对象']='' 设置下一页按钮禁止使用"
                        Log.debug(DEBUG)
                    else:
                        DB_INFO['数据库游标对象'] = 游标对象     # 缓存数据库游标对象
                        SV_数据库游标对象.set(str(游标对象))
                        按钮_显编框下一页['state'] = 'normal'    # 启用下一页按钮
                        DEBUG = f"游标对象={游标对象} 缓存游标(数据>=分页第一页)(用于继续读取显示下一页) 设置 DB_INFO['数据库游标对象']=游标对象 设置下一页按钮可以使用"
                        Log.debug(DEBUG)
                else:
                    字段和数据的存储和展示(字段名列表, [])
                    游标对象.close()                             # 关闭游标对象
                    DB_INFO['数据库游标对象'] = ''               # 清空数据库游标对象
                    SV_数据库游标对象.set('')
                    按钮_显编框下一页['state'] = 'disabled'      # 禁止下一页按钮
                    DEBUG = f"游标对象={游标对象} 关闭游标(数据=空,不用分页) 设置 DB_INFO['数据库游标对象']='' 设置下一页按钮禁止使用"
                    Log.debug(DEBUG)
            else:                                                # 分页限制行数 <= 0 读取全部记录,不分页
                全部记录 = 游标对象.fetchall()                   # 从SQL查询结果中取出全部的记录
                字段和数据的存储和展示(字段名列表, 全部记录)
                游标对象.close()                                 # 关闭游标对象
                DB_INFO['数据库游标对象'] = ''                   # 清空数据库游标对象
                SV_数据库游标对象.set('')
                按钮_显编框下一页['state'] = 'disabled'          # 禁止下一页按钮
                DEBUG = f"游标对象={游标对象} 关闭游标(用户通过设置分页行数<=0一次性读取全部数据) 设置 DB_INFO['数据库游标对象']='' 设置下一页按钮禁止使用"
                Log.debug(DEBUG)
                实际读取记录行数 = len(全部记录)                                ### 用于修改数据库后,再次显示在修改位置
                IV_已显示记录数.set(IV_已显示记录数.get() + 实际读取记录行数)   ### 用于修改数据库后,再次显示在修改位置
                IV_上次分页行数.set(实际读取记录行数)                           ### 用于修改数据库后,再次显示在修改位置
def 执行上条查询语句_刷新显示(重新定位要求):
    最后查询语句 = DB_INFO['最后查询语句']
    Log.debug(f"执行上条查询语句 DB_INFO['最后查询语句']={最后查询语句}")
    if 最后查询语句 != '':
        if 重新定位要求 == '从头开始':
            DEF_SQL_查询和显示(最后查询语句)
        elif 重新定位要求 == '已翻页位置开始':
            显示开始行定位 = IV_已显示记录数.get() - IV_上次分页行数.get()
            DEF_SQL_查询和显示(最后查询语句, 显示开始行定位)
def DEF_SQL_查询和显示_下一页():               # 分页操作:显示下一页
    游标对象 = DB_INFO['数据库游标对象']                                # 获取缓存的数据库游标对象
    if 游标对象 == '':
        CRITICAL = "DEF_SQL_查询和显示_下一页 DB_INFO['数据库游标对象']='' 保存的游标对象不存在,无法继续读取"
        Log.critical(CRITICAL)
        tkinter.messagebox.showerror(title='CRITICAL', message=CRITICAL)
    else:
        Log.debug(f"DEF_SQL_查询和显示_下一页 使用保存的游标对象 DB_INFO['数据库游标对象']={游标对象} 继续读取后续数据")
        字段名列表 = DB_INFO['字段名列表']                          # 从全局变量中取出保存的字段名信息
        分页限制行数 = IV_设置分页行数.get()
        if 分页限制行数 > 0:
            部分记录 = 游标对象.fetchmany(分页限制行数)
            实际读取记录行数 = len(部分记录)
            if 实际读取记录行数 == 0:
                游标对象.close()                                    ## 关闭游标对象
                DB_INFO['数据库游标对象'] = ''                      ## 清空数据库游标对象
                SV_数据库游标对象.set('')
                按钮_显编框下一页['state'] = 'disabled'             ## 禁止下一页按钮
                INFO = '已经到底,当前页已经是全部记录'
                tkinter.messagebox.showinfo(title='INFO', message=INFO)
                Log.debug(f"游标对象={游标对象} 关闭游标(数据=空,当前页已经是最后)")
            else:
                if 实际读取记录行数 < 分页限制行数:                 # 已经全部显示,没有后续分页
                    游标对象.close()                                ## 关闭游标对象
                    DB_INFO['数据库游标对象'] = ''                  ## 清空数据库游标对象
                    SV_数据库游标对象.set('')
                    按钮_显编框下一页['state'] = 'disabled'         ## 禁止下一页按钮
                    字段和数据的存储和展示(字段名列表, 部分记录)
                    Log.debug(f"游标对象={游标对象} 关闭游标(数据<分页显示行数)")
                else:                                               # 实际读取和限制显示相等,可能后面还有。可能刚刚读完
                    字段和数据的存储和展示(字段名列表, 部分记录)
                    Log.debug(f"游标对象={游标对象} 保持游标(数据>=分页显示行数)")
                    
                IV_已显示记录数.set(IV_已显示记录数.get() + 实际读取记录行数)   ### 用于修改数据库后,再次显示在修改位置
                IV_上次分页行数.set(实际读取记录行数)                           ### 用于修改数据库后,再次显示在修改位置
        else:
            全部记录 = 游标对象.fetchall()                          # 从SQL查询结果中取出全部的记录
            游标对象.close()                                        ## 关闭游标对象
            Log.debug(f"游标对象={游标对象} 关闭游标(用户通过设置分页行数<=0一次性读取全部余下数据)")
            DB_INFO['数据库游标对象'] = ''                          ## 清空数据库游标对象
            SV_数据库游标对象.set('')
            按钮_显编框下一页['state'] = 'disabled'                 ## 禁止下一页按钮
            if 全部记录 == []:
                INFO = '已经到底,当前页已经是全部记录'
                tkinter.messagebox.showinfo(title='INFO', message=INFO)
            else:
                字段和数据的存储和展示(字段名列表, 全部记录)
            实际读取记录行数 = len(全部记录)                                ### 用于修改数据库后,再次显示在修改位置
            IV_已显示记录数.set(IV_已显示记录数.get() + 实际读取记录行数)   ### 用于修改数据库后,再次显示在修改位置
            IV_上次分页行数.set(实际读取记录行数)                           ### 用于修改数据库后,再次显示在修改位置
def DEF_SQL_执行_成功自动提交(SQL_CMD):        # 非查询的SQL语句(执行一条SQL语句)(自动提交)(每次执行都要打开关闭新的游标)
    if SV_安全模式状态.get() == '关闭':
        数据库连接对象 = DB_INFO['数据库连接对象']
        try:
            游标对象 = 数据库连接对象.cursor()    # 创建一个游标
        except Exception as e:
            CRITICAL = f'创建游标失败 {e}'
            Log.critical(CRITICAL)
            return(1, CRITICAL)
        else:
            try:
                游标对象.execute(SQL_CMD)
            except Exception as e:
                ##失败情况不关闭游标,以免点击下一页失效
                ERROR = f'执行SQL语句 {SQL_CMD} 失败 {e}'
                Log.error(ERROR)
                TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
                Log.debug(f'游标对象={游标对象} 保持游标')
                return(1, ERROR)
            else:
                数据库连接对象.commit()             # 提交更改
                受影响行数 = '未知'
                if SV_数据库类型.get() != 'SQLite3':
                    受影响行数 = 游标对象.rowcount  # 获取受影响行数
                游标对象.close()
                DB_INFO['数据库游标对象'] = ''
                SV_数据库游标对象.set('')
                Log.debug(f'游标对象={游标对象} 关闭游标')
                INFO = f'执行SQL语句 {SQL_CMD} 成功 受影响行数={受影响行数}'
                TEXT_数据库操作日志.insert(0.0, INFO+'\n', 'tag_w')
                return(0, INFO)
    else:
        安全模式处理SQL操作([SQL_CMD])  ## 安全模式,任何修改数据库的命令不直接运行,都放入命令行文本框,由人工审核后再手动执行
        return(0, '未实际执行,仅写到SQL命令行中,执行需要设置安全模式=关闭')
def DEF_SQL_执行_不自动提交(SQL_CMD):          # 非查询的SQL语句(执行一条SQL语句)(不自动提交)(每次执行都要打开关闭新的游标)
    Log.debug(f'DEF_SQL_执行_不自动提交({SQL_CMD})')
    if SV_安全模式状态.get() == '关闭':
        数据库连接对象 = DB_INFO['数据库连接对象']
        try:
            游标对象 = 数据库连接对象.cursor()    # 创建一个游标
        except Exception as e:
            CRITICAL = f'创建游标失败 {e}'
            Log.critical(CRITICAL)
            return(1, CRITICAL)
        else:
            try:
                游标对象.execute(SQL_CMD)
            except Exception as e:
                ##失败情况不关闭游标,以免点击下一页失效
                ERROR = f'执行SQL语句 {SQL_CMD} 失败 {e}'
                Log.error(ERROR)
                TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
                Log.debug(f'游标对象={游标对象} 保持游标')
                return(1, ERROR)
            else:
                受影响行数 = '未知'
                if SV_数据库类型.get() != 'SQLite3':
                    受影响行数 = 游标对象.rowcount  # 获取受影响行数
                游标对象.close()
                DB_INFO['数据库游标对象'] = ''
                SV_数据库游标对象.set('')
                Log.debug(f'游标对象={游标对象} 关闭游标')
                INFO = f'执行SQL语句 {SQL_CMD} 成功 受影响行数={受影响行数}'
                TEXT_数据库操作日志.insert(0.0, INFO+'\n', 'tag_w')
                return(0, INFO)
    else:
        安全模式处理SQL操作([SQL_CMD])  ## 安全模式,任何修改数据库的命令不直接运行,都放入命令行文本框,由人工审核后再手动执行
        return(0, '未实际执行,仅写到SQL命令行中,执行需要设置安全模式=关闭')
def DEF_SQL_执行多条_忽略错误语句(L_SQL_CMD):  ## 非查询的SQL语句(执行多条SQL语句)(成功的提交更改,跳过错误的语句)## SQLite3 不能使用事件,无法回退
    L_成功信息 = []
    L_失败信息 = []
    if SV_安全模式状态.get() == '关闭':
        失败标记 = 0
        for SQL_CMD in L_SQL_CMD:
            R = DEF_SQL_执行_成功自动提交(SQL_CMD)
            if R[0] == 0:
                L_成功信息.append(R[1])
            else:
                L_失败信息.append(R[1])
        return(L_成功信息, L_失败信息)
    else:
        安全模式处理SQL操作(L_SQL_CMD)  ## 安全模式,任何修改数据库的命令不直接运行,都放入命令行文本框,由人工审核后再手动执行
        return(L_成功信息, L_失败信息)
def 安全模式处理SQL操作(L_SQL_CMD):            # 安全模式,任何修改数据库的命令不直接运行,都放入命令行文本框,由人工审核后再手动执行
    文本框_命令行.delete(0.0, END)
    TEXT = ';\n'.join(L_SQL_CMD)
    文本框_命令行.insert(0.0, TEXT+'\n')
    文本框_命令行.focus_set()
def DEF_SQLite3_执行脚本_不自动提交修改(SQL_SCRIPT):
    数据库连接对象 = DB_INFO['数据库连接对象']
    if 数据库连接对象 == '':
        CRITICAL = '数据库没有打开'
        Log.critical(CRITICAL)
        return(1, CRITICAL)
    else:
        try:
            游标对象 = 数据库连接对象.cursor()
        except Exception as e:
            CRITICAL = f'创建游标失败 {e}'
            Log.critical(CRITICAL)
            return(1, CRITICAL)
        else:
            try:
                游标对象.executescript(SQL_SCRIPT)      # SQLite3 执行SQL脚本
            except Exception as e:
                ##失败情况不关闭游标,以免点击下一页失效
                ERROR = f'执行SQL脚本 {SQL_SCRIPT} 失败 {e}'
                Log.error(ERROR)
                TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
                Log.debug(f'游标对象={游标对象} 保持游标')
                return(1, ERROR)
            else:
                ##数据库连接对象.commit()                 # 手动提交,确认修改
                游标对象.close()
                DB_INFO['数据库游标对象'] = ''
                SV_数据库游标对象.set('')
                Log.debug(f'游标对象={游标对象} 关闭游标')
                INFO = f'执行SQL脚本 {SQL_SCRIPT} 成功 需手动提交 commit()'
                TEXT_数据库操作日志.insert(0.0, INFO+'\n', 'tag_w')
                return(0, INFO)
def DEF_SQL_执行多条SQL语句_事件控制_不自动提交修改(SQL_CMD): # 批处理
    数据库连接对象 = DB_INFO['数据库连接对象']
    数据库连接对象.begin()                     # 开启事件,只对支持的事件的数据库引擎如 InnoDB 有效
    受影响行数 = 0
    try:
        游标对象 = 数据库连接对象.cursor()     # 创建一个游标
    except Exception as e:
        ERROR = '创建游标失败' + str(e)
        print(ERROR)
        return(1, ERROR)
    else:
        try:
            ## 以分号区分语句,尝试依次执行
            for 单条语句 in SQL_CMD.split(';'):
                单条语句整理 = 单条语句.strip()           # 去掉语句首尾空格
                if 单条语句整理 != '':
                    游标对象.execute(单条语句整理)
                    受影响行数 += 游标对象.rowcount
        except Exception as e:
            游标对象.close()
            数据库连接对象.rollback()      # 回退,撤销修改
            ERROR = f"执行出错:{e} 执行回退"
            TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
            return(1, ERROR)
        else:
            游标对象.close()
            ##数据库连接对象.commit()        # 手动提交,确认修改
            INFO = f'执行SQL脚本 成功 受影响行数={受影响行数} 【注意】需手动点 提交 按钮!!'
            TEXT_数据库操作日志.insert(0.0, INFO+'\n', 'tag_w')
            return(0, INFO)
def DEF_执行脚本或批处理_不自动提交修改(TEXT):
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        R = DEF_SQLite3_执行脚本_不自动提交修改(TEXT)
        return(R)
    elif 当前数据库类型 == 'MSSQL':
        数据库连接对象 = DB_INFO['数据库连接对象']
        print("数据库连接对象")
        for i in dir(数据库连接对象):
            print(f"  {i}")
        
        return(1, 'TEST MSSQL')
    else:
        R = DEF_SQL_执行多条SQL语句_事件控制_不自动提交修改(TEXT)
        return(R)

def FRAME_CLEAR(FRAME_NAME):        # 清空框内控件
    for X in FRAME_NAME.winfo_children():
        X.destroy()
def 字段和数据的存储和展示(L_字段名, LL_数据值): # 在显编框展示结果,保存结果到全局变量,可以进行修改操作
    if IV_急速查看模式_勾选框.get() == 0:    # 没有勾选
        列数 = len(L_字段名)
        行数 = len(LL_数据值)
        
        单元格宽度限制 = IV_单元格限宽.get()              # 单元格宽度限制字符数
        
        ## 记录字段中每个字段需要的Entry宽值
        L_字段需要宽值 = [计算字符串像素宽返回Entry标准宽(i) for i in L_字段名]
        Log.debug(f"L_字段名={L_字段名} L_字段需要宽值={L_字段需要宽值}")
        
        ## 遍历每一列,计算每列可用的最大宽值(取最大值,超过限制的用限制值)
        L_每列最大宽度 = []
        for 列号 in range(0, 列数):
            if L_字段需要宽值[列号] > 单元格宽度限制:  # 字段名已经超过最大限制值
                L_每列最大宽度.append(单元格宽度限制)  # 本列采用最大限制值
                #Log.debug(f"列号={列号} (字段名宽){L_字段需要宽值[列号]} > {单元格宽度限制}(单元格宽度限制) 采用最大限制值")
            else:
                ## 遍历每行指定列元素
                本列最大宽度 = L_字段需要宽值[列号]    # 字段名的宽设置为本列最大宽的初始值
                for 行号 in range(0, 行数):
                    字符串_值 = str(LL_数据值[行号][列号])
                    字符串宽值 = 计算字符串像素宽返回Entry标准宽(字符串_值)
                    if 字符串宽值 > 单元格宽度限制:    # 已经超过最大限制值
                        本列最大宽度 = 单元格宽度限制  # 本列采用最大限制值
                        #Log.debug(f"列号={列号} 行号={行号} 字符串_值={字符串_值} 字符串宽值={字符串宽值} > {单元格宽度限制}(单元格宽度限制) 采用最大限制值 并终止循环")
                        break   # 终止对本列的循环
                    else:
                        if 字符串宽值 > 本列最大宽度:
                            本列最大宽度 = 字符串宽值
                            #Log.debug(f"列号={列号} 行号={行号} 字符串_值={字符串_值} 字符串宽值={字符串宽值} 为最新最大宽值")
                L_每列最大宽度.append(本列最大宽度)
        Log.debug(f"计算完成 L_每列最大宽度={L_每列最大宽度}")
        DB_INFO['字段宽度记录'] = dict(zip(L_字段名,L_每列最大宽度))   # 保存结果后续新增数据窗口可用
        SV_字段宽度记录.set(str(DB_INFO['字段宽度记录']))              # DEBUG查看
        
        T0 = time.time()
        FRAME_CLEAR(LF_显示编辑框)                    # 清空框内控件
        Log.debug(f"清空框内控件 用时={time.time()-T0:.3f}秒")
        
        ## 创建画布
        画布 = Canvas(LF_显示编辑框, bg='#00CED1')    # 创建画布
        
        ## 在画布里创建 Frame
        画布Frame框 = Frame(画布)
        字段框 = Frame(画布Frame框)
        字段框.grid(row=0,column=0,sticky='NW')
        数据框 = Frame(画布Frame框)
        数据框.grid(row=1,column=0,sticky='NW')
        
        ## 动态设置画布窗口宽高:根据主主窗口的参数设置限宽限高
        主窗口大小和位置 = top.geometry()
        主窗口宽, 主窗口高, 主窗口X, 主窗口Y = re.findall('[0-9]+', 主窗口大小和位置)
        画图限高 = int(主窗口高) -450   # 预留下边控件空间
        
        ## 设置画布参数
        总行数 = 行数 + 1    # 数据行数n + 字段行数1
        
        ## 画布可滚动显示的最大宽和高(要刚好能放下画布里的Frame里的全部控件)
        ## 一个单元格(默认宽为144像素,20字符+4像素打底)
        ## Entry 字符宽度与像素关系:
        # Entry 最小宽度字符为1(设置为0也会自动设置为1)
        # 宽度 = 1字符 = 11像素(1*7 +4)
        # 宽度 = 2字符 = 18像素(2*7 +4)
        画布滚动最右边 = sum(L_每列最大宽度)*7 + 列数*4  # 总字符数量x9像素+每个单元格需要初始4像素
        画布滚动最下边 = 21*总行数                       # Entry 默认高度为 21像素(20像素+1分隔像素)
        
        ## 滚动能到的宽高必须>=元素总和宽高像素,不然会显示不出
        画布['scrollregion'] = (0,0,画布滚动最右边,画布滚动最下边)   # 一个元组 tuple (w, n, e, s) ,定义了画布可滚动的最大区域,w 为左边,n 为头部,e 为右边,s 为底部
        
        ## 动态设置显示画布固定显示宽和高(要和主显示框的大小匹配)
        画布['width'] = 画布滚动最右边                   # 使用实际需要的宽度即可
        画布['height'] = min(画图限高, 画布滚动最下边)   # 取最小值,防止纵向撑爆
        
        Scrollbar_画布_竖 = Scrollbar(LF_显示编辑框, command=画布.yview)   # 创建竖滚动条
        Scrollbar_画布_竖.grid(row=0,column=1,sticky='SWEN')               # 竖滚动条定位
        Scrollbar_画布_横 = Scrollbar(LF_显示编辑框, command=画布.xview, orient=HORIZONTAL) # 创建横滚动条
        Scrollbar_画布_横.grid(row=1,column=0,sticky='SWEN')                                # 横滚动条定位
        画布.config(xscrollcommand=Scrollbar_画布_横.set, yscrollcommand=Scrollbar_画布_竖.set) # 自动设置滚动幅度
        画布.create_window((0,0), window=画布Frame框, anchor='nw')    # 显示画布内 画布Frame框 控件
        画布.grid(row=0,column=0,sticky='nw')                         # 显示画布,不显示画布多余部分
        #画布.grid(row=0,column=0,sticky='nsew')                      # 显示画布,画布填满空间
    
        ## 清除全局字典的内容
        字典_查询字段_坐标_对象.clear()
        字典_查询字段_坐标_初值.clear()
        字典_查询结果_坐标_对象.clear()
        字典_查询结果_坐标_初值.clear()
        
        ## 字段名
        for 列 in range(0, 列数):
            初始值 = str(L_字段名[列])                                                                 # 转成字符串
            字典_查询字段_坐标_对象[(0,列)] = Entry(字段框, width=L_每列最大宽度[列], bg='#00BFFF')    # 控件对象放到指定框内,并保存对象到对象字典中,只读后颜色失效
            字典_查询字段_坐标_初值[(0,列)] = 初始值                                                   # 保存初始值
            字典_查询字段_坐标_对象[(0,列)].insert(0, 初始值)                                          # 写入Entry作为初始值(从标签内开头开始填充新值,因为是全新标签,所有不用先删除里面内容)
            字典_查询字段_坐标_对象[(0,列)].grid(row=0,column=列,sticky='W')                           # Entry排放到指定位置
            字典_查询字段_坐标_对象[(0,列)].bind("<Button-3>", DEF_鼠标事件_右键菜单_字段单元)         # 每个控件对象都绑定右键菜单事件
    
        ## 数据值
        for 行 in range(0, 行数):
            for 列 in range(0, 列数):
                初始值 = LL_数据值[行][列]
                if 初始值 == None:
                    初始值 = SV_自定义空值字符串表示.get()                                             # 设置自定义的空值的字符串表示,如 '(NULL)'
                else:
                    初始值 = str(LL_数据值[行][列])                                                    # 转成字符串
                字典_查询结果_坐标_对象[(行,列)] = Entry(数据框, width=L_每列最大宽度[列])             # 控件对象放到指定框内,并保存对象到对象字典中
                字典_查询结果_坐标_初值[(行,列)] = 初始值                                              # 保存初始值,被编辑时对比判断是否被修改
                字典_查询结果_坐标_对象[(行,列)].insert(0, 初始值)                                     # 写入Entry作为初始值(从标签内开头开始填充新值,因为是全新标签,所有不用先删除里面内容)
                字典_查询结果_坐标_对象[(行,列)].grid(row=行,column=列,sticky='W')                     # Entry排放到指定位置
                #字典_查询结果_坐标_对象[(行,列)].bind("<Button-1>", DEF_鼠标事件_左键单击)            # 每个控件对象都绑定左键单击事件(不用了)
                #字典_查询结果_坐标_对象[(行,列)].bind('<Enter>', DEF_鼠标事件_光标悬停)               # 每个控件对象都绑定光标悬停事件(尝试中)
                字典_查询结果_坐标_对象[(行,列)].bind("<Button-3>", DEF_鼠标事件_右键菜单_数据单元)    # 每个控件对象都绑定右键菜单事件
                字典_查询结果_坐标_对象[(行,列)].bind('<KeyRelease>', DEF_键盘事件_任意输入_数据单元)  # 每当输入内容时执行一次函数
    else:
        FRAME_CLEAR(LF_显示编辑框)                     # 清空框内控件
        
        单元格宽度限制 = IV_单元格限宽.get()           # 单元格宽度限制字符数
        
        列数 = len(L_字段名)
        行数 = len(LL_数据值)
        
        ## 记录字段中每个字段需要的Entry宽值
        L_字段需要宽值 = [计算字符串像素宽返回Entry标准宽(i) for i in L_字段名]
        Log.debug(f"L_字段名={L_字段名} L_字段需要宽值={L_字段需要宽值}")
        
        ## 遍历每一列,计算每列可用的最大宽值(取最大值,超过限制的用限制值)
        L_每列最大宽度 = []
        for 列号 in range(0, 列数):
            if L_字段需要宽值[列号] > 单元格宽度限制:  # 字段名已经超过最大限制值
                L_每列最大宽度.append(单元格宽度限制)  # 本列采用最大限制值
                #Log.debug(f"列号={列号} (字段名宽){L_字段需要宽值[列号]} > {单元格宽度限制}(单元格宽度限制) 采用最大限制值")
            else:
                ## 遍历每行指定列元素
                本列最大宽度 = L_字段需要宽值[列号]    # 字段名的宽设置为本列最大宽的初始值
                for 行号 in range(0, 行数):
                    字符串_值 = str(LL_数据值[行号][列号])
                    字符串宽值 = 计算字符串像素宽返回Entry标准宽(字符串_值)
                    if 字符串宽值 > 单元格宽度限制:    # 已经超过最大限制值
                        本列最大宽度 = 单元格宽度限制  # 本列采用最大限制值
                        #Log.debug(f"列号={列号} 行号={行号} 字符串_值={字符串_值} 字符串宽值={字符串宽值} > {单元格宽度限制}(单元格宽度限制) 采用最大限制值 并终止循环")
                        break   # 终止对本列的循环
                    else:
                        if 字符串宽值 > 本列最大宽度:
                            本列最大宽度 = 字符串宽值
                            #Log.debug(f"列号={列号} 行号={行号} 字符串_值={字符串_值} 字符串宽值={字符串宽值} 为最新最大宽值")
                L_每列最大宽度.append(本列最大宽度)
        Log.debug(f"计算完成 L_每列最大宽度={L_每列最大宽度}")
        DB_INFO['字段宽度记录'] = dict(zip(L_字段名,L_每列最大宽度))   # 保存结果后续新增数据窗口可用
        SV_字段宽度记录.set(str(DB_INFO['字段宽度记录']))              # DEBUG查看
        
        ## 动态设置画布窗口宽高:根据主主窗口的参数设置限宽限高
        主窗口大小和位置 = top.geometry()
        主窗口宽, 主窗口高, 主窗口X, 主窗口Y = re.findall('[0-9]+', 主窗口大小和位置)
        表格限高 = int(主窗口高) -450   # 预留下边控件空间
        
        最佳高度 = min(行数, 表格限高//21)
        
        xscroll = Scrollbar(LF_显示编辑框, orient=HORIZONTAL)
        yscroll = Scrollbar(LF_显示编辑框, orient=VERTICAL)
        
        TV_数据表格 = ttk.Treeview(
        master=LF_显示编辑框,       # 父容器
        height=最佳高度,            # 表格显示部分的高(单位:行数,不显示部分用滚动条查看)
        columns=L_字段名,           # 显示的列
        show='headings',            # 隐藏首列
        xscrollcommand=xscroll.set, # x轴滚动条
        yscrollcommand=yscroll.set, # y轴滚动条
        )
        L_INDEX_N = 0
        for column in L_字段名:
            TV_数据表格.heading(column=column, text=column, anchor=W)   # 定义表头
            TV_数据表格_列宽 = L_每列最大宽度[L_INDEX_N]*8
            TV_数据表格.column(column=column, anchor=W, width=TV_数据表格_列宽, minwidth=TV_数据表格_列宽)  # 定义列
            L_INDEX_N += 1
        
        for DATA in LL_数据值:
            TV_数据表格.insert('', END, values=DATA)  # 添加数据到末尾
        
        ## 嵌套函数
         
        def 复制到剪贴板(选中值):
            top.clipboard_clear()           # 清空剪贴板
            top.clipboard_append(选中值)    # 写入剪贴板
            print(f"已经复制到剪贴板={选中值}")
        
        def DEF_嵌套_鼠标左键双击(event):
            选中IID = TV_数据表格.identify_row(event.y)
            选中列 = TV_数据表格.identify_column(event.x)
            列号_PY_INDEX = int(选中列[1:])-1
            SV_TV_数据表格_表头_选中值.set(DB_INFO['字段名列表'][列号_PY_INDEX])
            Log.debug(f"鼠标左键双击 选中IID={选中IID} 选中列={选中列} 列号_PY_INDEX={列号_PY_INDEX}")
            if 选中IID:
                L_VALUES = TV_数据表格.item(选中IID)['values']
                选中值 = L_VALUES[列号_PY_INDEX]
            else:
                print("【点出界外】应该的点在表头行了")
                选中值 = DB_INFO['字段名列表'][列号_PY_INDEX]
            复制到剪贴板(选中值)
        
        def DEF_嵌套_鼠标右键(event):
            print("TV_数据表格.identify_row(event.y)", TV_数据表格.identify_row(event.y))
            print("TV_数据表格.identify_column(event.x)", TV_数据表格.identify_column(event.x))
            选中IID = TV_数据表格.identify_row(event.y)
            选中列 = TV_数据表格.identify_column(event.x)
            列号_PY_INDEX = int(选中列[1:])-1
            SV_TV_数据表格_表头_选中值.set(DB_INFO['字段名列表'][列号_PY_INDEX])
            Log.debug(f"鼠标右键 选中IID={选中IID} 选中列={选中列} 列号_PY_INDEX={列号_PY_INDEX}")
            if 选中IID:
                TV_数据表格.selection_set(选中IID)
                L_VALUES = TV_数据表格.item(选中IID)['values']
                点击格内容 = L_VALUES[列号_PY_INDEX]
                print(f"L_VALUES={L_VALUES} 点击格内容={点击格内容}")
                SV_TV_数据表格_选中值.set(点击格内容)
            else:
                print("【点出界外】应该的点在表头行了")
                SV_TV_数据表格_选中值.set(DB_INFO['字段名列表'][列号_PY_INDEX])
                
            ## 弹出菜单
            光标X轴 = event.x_root
            光标Y轴 = event.y_root
            右键菜单.post(光标X轴, 光标Y轴)    # 光标位置显示菜单
        
        def DEF_表值_右键菜单_复制单元格值():
            单元格值 = SV_TV_数据表格_选中值.get()
            复制到剪贴板(单元格值)
        
        def DEF_嵌套_右键菜单_此分页数据导出为CSV文件():
            默认导出文件名 = '导出部分数据.csv'
            导出文件名 = tkinter.simpledialog.askstring(title='导出文件名(重名会覆盖)', prompt='请输入导出文件名:', initialvalue=默认导出文件名)
            ## 确定为输入内容,取消为None
            if 导出文件名 != None:
                CSV写入本地(导出文件名, L_字段名, LL_数据值)
        
        右键菜单 = Menu(tearoff=False)
        右键菜单.add_command(label='复制单元格值(或双击左键)', command=DEF_表值_右键菜单_复制单元格值)
        右键菜单.add_separator()
        右键菜单.add_command(label='统计本列出现次数', command=DEF_右键菜单_统计本列出现次数)
        右键菜单.add_separator()
        右键菜单.add_command(label='sql: 列值精确匹配 IS/=', command=DEF_右键菜单_列值精确匹配)    ## 生成精确匹配此列的SQL语句
        右键菜单.add_command(label='sql: 列值模糊匹配 LIKE', command=DEF_右键菜单_列值模糊匹配)
        右键菜单.add_separator()
        右键菜单.add_command(label='此分页数据导出(CSV)', command=DEF_嵌套_右键菜单_此分页数据导出为CSV文件)       
        
        TV_数据表格.bind('<Double-Button-1>', DEF_嵌套_鼠标左键双击)   # 绑定左键双击事件
        TV_数据表格.bind('<Button-3>', DEF_嵌套_鼠标右键)              # 打开菜单
        
        xscroll.config(command=TV_数据表格.xview)
        xscroll.pack(side=BOTTOM, fill=X)
        yscroll.config(command=TV_数据表格.yview)
        yscroll.pack(side=RIGHT, fill=Y)
        TV_数据表格.pack(fill=BOTH, expand=True)    # 混用了pack,这里pack才有横滚动条效果
def 生成_库名表名限定SQL语句部分(): # 数据库名.数据表名
    数据库名 = SV_数据库树_选中库名.get()
    数据表名 = SV_数据库树_选中表名.get()
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        SQL_库名表名 = 数据表名
    elif 当前数据库类型 == 'MSSQL':
        SQL_库名表名 = f'{数据库名}..{数据表名}'
    else:
        SQL_库名表名 = f'{数据库名}.{数据表名}'
        WARNING = f'{当前数据库类型}类型数据库 {数据库名}.{数据表名}'
        TEXT_数据库操作日志.insert(0.0, WARNING+'\n', 'tag_w')
    return(SQL_库名表名)
def 生成_库名表名限定SQL语句部分_建表用():
    数据库名 = SV_新建数据表_库名.get()
    数据表名 = SV_新建数据表_表名.get()
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        SQL_库名表名 = 数据表名
    elif 当前数据库类型 == 'MSSQL':
        SQL_库名表名 = f'{数据库名}..{数据表名}'
    else:
        SQL_库名表名 = f'{数据库名}.{数据表名}'
        WARNING = f'{当前数据库类型}类型数据库 {数据库名}.{数据表名}'
        TEXT_数据库操作日志.insert(0.0, WARNING+'\n', 'tag_w')
    return(SQL_库名表名)
def 生成_查表SQL语句():
    数据库名 = SV_数据库树_选中库名.get()
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        SQL_CMD = 'SELECT NAME FROM sqlite_master'  # SQLite3查表命令,返回数据结构 [('sqlite_sequence',), ('t0',), ('t1',)]
    elif 当前数据库类型 == 'MySQL':
        SQL_CMD = f'SHOW TABLES FROM {数据库名};'   # MySQL查表命令,返回数据结构 (('t',), ('t2',))
    elif 当前数据库类型 == 'Oracle':
        SQL_CMD = f'SELECT table_name FROM user_tables'
    elif 当前数据库类型 == 'MSSQL':
        SQL_CMD = f"SELECT name FROM {数据库名}..sysobjects WHERE xtype='U'"
    elif 当前数据库类型 == 'PostgreSQL':
        SQL_CMD = f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{数据库名}'"
    else:
        SQL_CMD = ''
        ERROR = f'未知{当前数据库类型}类型数据库的查询表名的SQL语句'
        TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
    return(SQL_CMD)
def 生成_查表内容SQL语句():
    数据库名 = SV_数据库树_选中库名.get()
    数据表名 = SV_数据库树_选中表名.get()
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        SQL_CMD = f'SELECT * FROM {数据表名}'
    elif 当前数据库类型 == 'MySQL':
        SQL_CMD = f'SELECT * FROM {数据库名}.{数据表名}'
    elif 当前数据库类型 == 'Oracle':
        SQL_CMD = f'SELECT * FROM {数据库名}.{数据表名}'
    elif 当前数据库类型 == 'MSSQL':
        SQL_CMD = f'SELECT * FROM {数据库名}..{数据表名}'
    elif 当前数据库类型 == 'PostgreSQL':
        SQL_CMD = f'SELECT * FROM {数据库名}.{数据表名}'
    else:
        SQL_CMD = ''
        ERROR = f'未知{当前数据库类型}类型数据库的查询表内容的SQL语句'
        TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
    return(SQL_CMD)
def 生成_查表字段属性SQL语句(备注):
    数据库名 = SV_数据库树_选中库名.get()
    数据表名 = SV_数据库树_选中表名.get()
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        SQL_CMD = f'PRAGMA table_info({数据表名})'      # SQLite3 查询字段属性的命令语句
    elif 当前数据库类型 == 'MySQL':
        if 备注 == '主要6项':        ## 表名, 字段名, 字段类型, 是否可空, 默认值, 是否主键
            SQL_CMD = f"SELECT TABLE_SCHEMA, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY FROM information_schema.columns WHERE TABLE_SCHEMA='{数据库名}' AND TABLE_NAME='{数据表名}'"
        else:
            SQL_CMD = f"SELECT * FROM information_schema.columns WHERE TABLE_SCHEMA='{数据库名}' AND TABLE_NAME='{数据表名}'" # MySQL 查询字段属性的SQL语句
    elif 当前数据库类型 == 'Oracle':
        if 备注 == '主要6项':
            SQL_CMD = f""    # ?? COLUMN_NAME, (DATA_TYPE, DATA_LENGTH), NULLABLE, DATA_DEFAULT, 
        else:
            SQL_CMD = f"SELECT * FROM user_TAB_COLUMNS WHERE TABLE_NAME='{数据表名}'"   # Oracle 查询字段属性的SQL语句
    elif 当前数据库类型 == 'MSSQL':
        if 备注 == '主要6项':
            SQL_CMD = f""    # ??
        else:
            SQL_CMD = f"SELECT * FROM information_schema.columns WHERE TABLE_CATALOG='{数据库名}' AND TABLE_NAME='{数据表名}'"   # MSSQL 查询字段属性的SQL语句
    elif 当前数据库类型 == 'PostgreSQL':
        if 备注 == '主要6项':
            SQL_CMD = f""    # ??
        else:
            SQL_CMD = f"SELECT * FROM information_schema.columns WHERE table_schema='{数据库名}' AND table_name='{数据表名}'"
    else:
        SQL_CMD = ''
        ERROR = f'未知{当前数据库类型}类型数据库的查询表字段属性的SQL语句'
        TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
    return(SQL_CMD)
def 查表字段属性生成建表SQL语句():
    SQL_CREATE_TABLE = ''
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'MySQL':
        SQL_CMD = f"SHOW CREATE TABLE {SQL_库名表名}"
        R = DEF_SQL_查询和返回(SQL_CMD)
        Log.debug(f"MySQL 查建表信息结果 {R[1]}")
        if R[0] == 0:
            SQL_CREATE_TABLE = R[1][0][1] +';\n'
    elif 当前数据库类型 == 'SQLite3':
        SQL_CMD = f"SELECT sql FROM sqlite_master where name='{SQL_库名表名}'"
        R = DEF_SQL_查询和返回(SQL_CMD)
        Log.debug(f"SQLite3 查建表信息结果 {R[1]}")
        if R[0] == 0:
            SQL_CREATE_TABLE = R[1][0][0] +';\n'
    else:
        SQL_CREATE_TABLE = f"-- 尝试拼接生成{当前数据库类型}建表语句\n"
        SQL_CMD = 生成_查表字段属性SQL语句('主要6项')
        if SQL_CMD != '':
            R = DEF_SQL_查询和返回(SQL_CMD)
            if R[0] == 0:
                SQL_CREATE_TABLE += f'CREATE TABLE {SQL_库名表名} (\n'
                L_主键 = []
                Log.debug(f"查字段主要属性结果 {R[1]}")
                for 占位,字段名,字段类型,是否为空,默认值,是否主键 in R[1]:
                    ##print(占位,字段名,字段类型,是否为空,默认值,是否主键)
                    if 是否为空 == 1:
                        是否为空 = 'NOT NULL'
                    else:
                        是否为空 = 'NULL'
                    if 默认值 == None:
                        默认值 = ''
                    else:
                        if 默认值[0] == '"' and 默认值[-1] == '"':
                            默认值删除首尾双引号 = 默认值.strip('"')
                            默认值 = f"DEFAULT '{默认值删除首尾双引号}'"
                        else:
                            默认值 = f"DEFAULT '{默认值}'"
                    if 是否主键 != 0:
                        L_主键.append(字段名)
                    SQL_CREATE_TABLE += f"    {字段名} {字段类型} {是否为空} {默认值},\n"
                if L_主键 != []:
                    SQL_CREATE_TABLE += f"    PRIMARY KEY ({','.join(L_主键)}));\n"
                else:
                    SQL_CREATE_TABLE += f"    );\n"
        else:
            SQL_CREATE_TABLE = f"-- {当前数据库类型} 生成建表语句未完成\n"
    return(SQL_CREATE_TABLE)
def 查表数据内容生成批量插入SQL语句():
    SQL_INSERT = ''
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        SQL_CMD = f'SELECT * FROM {SQL_库名表名}'
        R = DEF_SQL_查询和返回_数据列表_字段列表(SQL_CMD)
        if R[0] == 0:
            ##print("表内数据查询结果", R[1])
            if R[1] != ():
                LL_数据_STR = ['\n'+str(tuple([j if j!=None else '替换占位字符串_PYNone转SQLnull' for j in i])) for i in R[1]]     # 遇到空值 None 换 字符串 '替换占位字符串_PYNone转SQLnull'
                ##print("LL_数据_STR", LL_数据_STR)
                L_字段名 = R[2]
                SQL_INSERT = f"INSERT INTO {SQL_库名表名} VALUES {','.join(LL_数据_STR)};"    # MySQL 插入语句数据部分含分号会报错
                SQL_INSERT = SQL_INSERT.replace("\'替换占位字符串_PYNone转SQLnull\'", "NULL")
            else:
                SQL_INSERT = "-- 数据内容: 无"
    return(SQL_INSERT)


####################
## TKinter 主窗口 ##
####################
top = Tk()  # 初始化Tk()
top.title('Python3 tkinter GUI 图形化操作 SQLite3/MySQL/Oracle/SQLServer/PostgreSQL 工具')   # 设置标题
窗口宽 = 900
窗口高 = 600
# 获取屏幕尺寸以计算布局参数,使窗口居屏幕中央
屏幕宽 = top.winfo_screenwidth()
屏幕高 = top.winfo_screenheight()
主窗口显示位置 = '%dx%d+%d+%d' % (窗口宽, 窗口高, (屏幕宽-窗口宽)/2, (屏幕高-窗口高)/2)
top.geometry(主窗口显示位置)
top.resizable(width=True, height=True)  # 设置窗口是否可变长、宽(True:可变,False:不可变)
top.grid_columnconfigure(1, weight=1)   # (第1列, 自适应窗口宽)

##################################################################################
## TKinter 主窗口布局 - TOP框 ####################################################
顶级功能菜单框  = Frame(top)
LF_数据库信息框 = LabelFrame(top, text='数据库信息框')
LF_日志框       = LabelFrame(top, text='数据库操作记录(倒序)')
常用功能框      = Frame(top)
LF_显示编辑框   = LabelFrame(top, text='数据: 显示/编辑框', bg='PaleGreen')
控表按钮框      = Frame(top)                        # 分页按钮/确认修改按钮
命令框          = LabelFrame(top, text='SQL语句/SQL脚本')
顶级功能菜单框.grid( row=0,column=1,sticky='NW')
LF_数据库信息框.grid(row=1,column=1,sticky='NW')
LF_日志框.grid(      row=2,column=1,sticky='NSEW')  # NSEW填满(自动伸缩必要设置)
常用功能框.grid(     row=3,column=1,sticky='NW')
LF_显示编辑框.grid(  row=4,column=1,sticky='NW')
控表按钮框.grid(     row=5,column=1,sticky='NW')
命令框.grid(         row=6,column=1,sticky='NSEW')
LF_日志框.grid_columnconfigure(0, weight=1)        # 0列横向自动填充
LF_显示编辑框.grid_columnconfigure(0, weight=1)    # 0列横向自动填充
命令框.grid_columnconfigure(0, weight=1)           # 0列横向自动填充
左框 = Frame(top)
左框.grid(row=1,column=0,rowspan=6,sticky='NSEW')  # 定位底框填充
Frame_数据库和表显示框 = LabelFrame(左框, text='数据库/数据表')
Frame_数据库和表显示框.grid(row=0,column=0,sticky='NSEW')
Frame_数据库和表显示框.grid_rowconfigure(0, weight=1)   # 0列纵向自动填充
左框.grid_rowconfigure(0, weight=1)

################################################################################################################
## Python 全局变量 ##
DB_INFO = {}                     ## 存储数据库信息
DB_INFO['数据库连接对象'] = ''   # 保存打开的数据库连接对象,方便后续直接调用
DB_INFO['数据库游标对象'] = ''   # 保存当前的数据库游标对象,方便往下翻页
DB_INFO['字段名列表'] = []       # 保持上一次成功查询的字段名
DB_INFO['最后查询语句'] = ''     # 记录上次成功执行查询语句,方便在修改数据表后能自动刷新到相同页面
DB_INFO['字段宽度记录'] = {}     # 记录上次显示的字段宽度信息,方便添加数据时使用相同宽度
DB_INFO['数据库列表'] = []       # 记录数据库服务器上的所有数据库
字典_查询字段_坐标_对象 = {}     # KEY=控件坐标 || VAULE=控件对象 || {(控件行号,控件列号):控件对象} || { (0,0):obj }
字典_查询字段_坐标_初值 = {}     # KEY=控件坐标 || VAULE=初始值   || {(控件行号,控件列号):初始值}   || { (0,0):123 }
字典_查询结果_坐标_对象 = {}
字典_查询结果_坐标_初值 = {}
字典_添加记录_坐标_对象 = {}
字典_创建表_字段信息 = {}
字典_新加字段信息 = {}
字典_对象存储 = {}               # {'文本编辑对象':''} 大文本编辑框用
数据字典_DB_TABLE_字段类型 = {}  # 不同的数据库提供不同的字段类型供选择
数据字典_DB_TABLE_字段类型['SQLite3'] = ('INTEGER', 'FLOAT', 'CHAR()', 'VARCHAR()', 'TEXT', 'DATE', 'timestamp', 'BLOB')
数据字典_DB_TABLE_字段类型['MySQL']   = ('INT','FLOAT','CHAR()','VARCHAR()','TEXT','DATE','TIME','TINYINT','SMALLINT','MEDIUMINT','BIGINT','DOUBLE','DECIMAL','YEAR','DATETIME','TIMESTAMP','TINYBLOB','TINYTEXT','BLOB','MEDIUMBLOB','MEDIUMTEXT','LONGBLOB','LONGTEXT')
数据字典_DB_TABLE_字段类型['Oracle']  = ('INTEGER','FLOAT','CHAR()','VARCHAR()','VARCHAR2(N)','NCHAR','NVARCHAR2','DATE','LONG','RAW','LONG','BLOB','CLOB','NCLOB','BFILE','ROWID','NROWID','NUMBER(P,S)','DECIMAL(P,S)','REAL')
数据字典_DB_TABLE_字段类型['MSSQL']   = ('int','float','char()','varchar()','text','bit','tinyint','smallint','real','decimal','numeric','smallmoney','money','Smalldatetime','datetime','cursor','timestamp','Uniqueidentifier','nchar','nvarchar','ntext','binary','varbinary','image')
数据字典_DB_TABLE_字段类型['PostgreSQL'] = ('integer','char()','varchar()','text','date','smallint','bigint','decimal','numeric','real','double precision','smallserial','serial','bigserial','money','character varying(n)','character(n)','timestamp [(p)]','time [(p)]','interval','boolean','point','line','lseg','box','path','polygon','circle','cidr','inet','macaddr','tsvector','tsquery')
数据字典_DB_TABLE_字段类型['Other'] = ('INT', 'FLOAT', 'CHAR(N)', 'VARCHAR(N)', 'TEXT', 'DATE')

################################
## TKinter 实时更新的全局变量 ##
SV_数据库类型 = StringVar()      # 当前操作的数据库类型:SQLite3/MySQL/Oracle/SQLServer/...
SV_数据库连接对象 = StringVar()  # DEBUG查看数据库连接对象转文本
SV_数据库游标对象 = StringVar()  # DEBUG查看数据库游标对象转文本
SV_最后查询语句 = StringVar()    # 记录上一次的查询语句,用于在修改后刷新显示编辑框内容
SV_查询字段列表 = StringVar()    # 查询语句查询结果的字段信息
SV_数据库列表 = StringVar()      # DEBUG查看数据库列表转文本
SV_数据表列表 = StringVar()      # DEBUG查看数据表列表转文本
字段框_定位列 = IntVar()
数据框_定位行 = IntVar()
数据框_定位列 = IntVar()
SV_新建数据表_库名 = StringVar()
SV_新建数据表_表名 = StringVar()
SV_新建数据表_存储引擎 = StringVar()
SV_新建数据表_字符集 = StringVar()
SV_字段宽度记录 = StringVar()         # DEBUG查看字段宽度记录字典转文本
SV_自定义空值字符串表示 = StringVar() # 自定义空值字符串表示
SV_自定义空值字符串表示.set('(NULL)') # 默认设置:遇到数据库内空值时用'(NULL)'表示,方便操作
SV_设置分页行数 = StringVar()       # 分页 设置要读取数据的行数(用户输入)
IV_设置分页行数 = IntVar()          # 分页 设置要读取数据的行数(用户输入提取数字部分)
SV_设置分页行数.set('10')
IV_设置分页行数.set(10)             # 设置默认值
IV_已显示记录数 = IntVar()          # 分页 用于修改数据库后,再次显示在修改位置
IV_上次分页行数 = IntVar()          # 分页 用于修改数据库后,再次显示在修改位置
DB_SQL_SCRIPT_FILE = StringVar()    # 选择本地SQL脚本文件
IV_光标X轴 = IntVar()               # 光标位置记录(当前就是看看,没有实际用途)
IV_光标Y轴 = IntVar()               # 光标位置记录(当前就是看看,没有实际用途)
IV_单元格限宽 = IntVar()            # 自动设置单元格宽度时的最大宽度(字符数)
IV_单元格限宽.set(20)               # 设置默认最大20字符宽度
SV_提示信息_建表 = StringVar()      ## 创建表/复制表提示信息
SV_数据库树_选中库名 = StringVar()  # 当前选择的库名
SV_数据库树_选中库名.set('无')
SV_数据库树_选中表名 = StringVar()  # 当前选择的表名
SV_数据库树_选中表名.set('无')
选中_TV_IID = StringVar()           # Treeview 保存IID变量,方便作为参数使用
SV_搜索内容 = StringVar()
SV_提示信息 = StringVar()
SV_登录帐号 = StringVar()
SV_登录密码 = StringVar()
SV_登录地址 = StringVar()
SV_服务端口 = StringVar()
SV_登录库名 = StringVar()
SV_字符集   = StringVar()
SV_SQLite3_数据库路径 = StringVar() # 当前操作的SQLite3数据库全路径
SV_MySQL_CONF_PATH = StringVar()    # MySQL连接配置文件路径
SV_MSSQL_CONF_PATH = StringVar()    # MSSQL(SQLServer)连接配置文件路径
SV_PostgreSQL_CONF_PATH = StringVar()   # PostgreSQL连接配置文件路径
SV_Oracle_CONF_PATH = StringVar()   # ORACLE连接配置文件路径
IV_急速查看模式_勾选框 = IntVar()
SV_TV_数据表格_选中值 = StringVar()
SV_TV_数据表格_表头_选中值 = StringVar()
################################################################################################################

## 全局变量 DEBUG 显示
def DEF_显示DEBUG小窗():
    新窗口 = Toplevel()
    新窗口.title('DEBUG')
    显示坐标 = '+500+200'
    新窗口.geometry(显示坐标)
    新窗口.grid_columnconfigure(0, weight=1)   # (第0列, 自适应窗口宽)
    新窗口.grid_rowconfigure(0, weight=1)      # (第0列, 自适应窗口高)
    
    LabelFrame_DEBUG框 = LabelFrame(新窗口, text='Debug 全局变量', bg='#FFD700')
    LabelFrame_DEBUG框.grid(row=0,column=0,sticky='NSEW')
    LabelFrame_DEBUG框.grid_columnconfigure(0, weight=1)
    LabelFrame_DEBUG框.grid_rowconfigure(0, weight=1)
    
    DEBUG画布 = Canvas(LabelFrame_DEBUG框, bg='#00CED1')    # 创建画布
    ## 在画布里创建 Frame
    DEBUG画布Frame框 = Frame(DEBUG画布)
    全局变量显示宽 = 80
    全局变量展示框 = Frame(DEBUG画布Frame框)
    全局变量展示框.grid(row=0,column=0,sticky='NW')
    Label(全局变量展示框, text='[数据库类型]').grid(          row=0,column=0,sticky='W')
    Label(全局变量展示框, text='[数据库连接对象]').grid(      row=1,column=0,sticky='W')
    Label(全局变量展示框, text='[数据库游标对象]').grid(      row=2,column=0,sticky='W')
    Label(全局变量展示框, text='[数据库树_选中库名]').grid(   row=3,column=0,sticky='W')
    Label(全局变量展示框, text='[数据库树_选中表名]').grid(   row=4,column=0,sticky='W')
    Label(全局变量展示框, text='[查询字段列表]').grid(        row=5,column=0,sticky='W')
    Label(全局变量展示框, text='[字段宽度记录]').grid(        row=6,column=0,sticky='W')
    Label(全局变量展示框, text='[最后查询语句]').grid(        row=7,column=0,sticky='W')
    Label(全局变量展示框, text='[数据库列表]').grid(          row=8,column=0,sticky='W')
    Label(全局变量展示框, text='[数据表列表]').grid(          row=9,column=0,sticky='W')
    Label(全局变量展示框, text='[新建数据表_库名]').grid(     row=10,column=0,sticky='W')
    Label(全局变量展示框, text='[新建数据表_表名]').grid(     row=11,column=0,sticky='W')
    Label(全局变量展示框, text='[新建数据表_存储引擎]').grid( row=12,column=0,sticky='W')
    Label(全局变量展示框, text='[新建数据表_字符集]').grid(   row=13,column=0,sticky='W')
    Label(全局变量展示框, text='[选中_TV_IID]').grid(         row=14,column=0,sticky='W')
    Label(全局变量展示框, text='[显编框.字段框定位 列]').grid(row=15,column=0,sticky='W')
    Label(全局变量展示框, text='[显编框.数据框定位 行]').grid(row=16,column=0,sticky='W')
    Label(全局变量展示框, text='[显编框.数据框定位 列]').grid(row=17,column=0,sticky='W')
    Label(全局变量展示框, text='[IV_光标X轴]').grid(          row=18,column=0,sticky='W')
    Label(全局变量展示框, text='[IV_光标Y轴]').grid(          row=19,column=0,sticky='W')
    Label(全局变量展示框, text='[IV_已显示记录数]').grid(     row=20,column=0,sticky='W')
    Label(全局变量展示框, text='[IV_上次分页行数]').grid(     row=21,column=0,sticky='W')
    Label(全局变量展示框, text='[SV_设置分页行数] 输入值').grid(row=22,column=0,sticky='W')
    Label(全局变量展示框, text='[IV_设置分页行数] 转数字').grid(row=23,column=0,sticky='W')
    Label(全局变量展示框, text='[单元格限宽]').grid(          row=24,column=0,sticky='W')
    Label(全局变量展示框, text='[SQLite3数据库路径]').grid(   row=25,column=0,sticky='W')
    Label(全局变量展示框, text='[SV_TV_数据表格_选中值]').grid(row=26,column=0,sticky='W')
    Label(全局变量展示框, text='[SV_TV_数据表格_表头_选中值]').grid(row=27,column=0,sticky='W')
    Entry(全局变量展示框, textvariable=SV_数据库类型,    width=全局变量显示宽).grid(row=0,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_数据库连接对象,width=全局变量显示宽).grid(row=1,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_数据库游标对象,width=全局变量显示宽).grid(row=2,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_数据库树_选中库名, width=全局变量显示宽).grid(row=3,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_数据库树_选中表名, width=全局变量显示宽).grid(row=4,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_查询字段列表,  width=全局变量显示宽).grid(row=5,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_字段宽度记录,  width=全局变量显示宽).grid(row=6,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_最后查询语句,  width=全局变量显示宽).grid(row=7,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_数据库列表,    width=全局变量显示宽).grid(row=8,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_数据表列表,    width=全局变量显示宽).grid(row=9,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_新建数据表_库名,    width=全局变量显示宽).grid(row=10,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_新建数据表_表名,    width=全局变量显示宽).grid(row=11,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_新建数据表_存储引擎,width=全局变量显示宽).grid(row=12,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_新建数据表_字符集,  width=全局变量显示宽).grid(row=13,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=选中_TV_IID,      width=全局变量显示宽).grid(row=14,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=字段框_定位列,    width=全局变量显示宽).grid(row=15,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=数据框_定位行,    width=全局变量显示宽).grid(row=16,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=数据框_定位列,    width=全局变量显示宽).grid(row=17,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=IV_光标X轴,       width=全局变量显示宽).grid(row=18,column=1,sticky='W',columnspan=3)
    Entry(全局变量展示框, textvariable=IV_光标Y轴,       width=全局变量显示宽).grid(row=19,column=1,sticky='W',columnspan=3)
    Entry(全局变量展示框, textvariable=IV_已显示记录数,  width=全局变量显示宽).grid(row=20,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=IV_上次分页行数,  width=全局变量显示宽).grid(row=21,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_设置分页行数,  width=全局变量显示宽).grid(row=22,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=IV_设置分页行数,  width=全局变量显示宽).grid(row=23,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=IV_单元格限宽,    width=全局变量显示宽).grid(row=24,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_SQLite3_数据库路径, width=全局变量显示宽).grid(row=25,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_TV_数据表格_选中值, width=全局变量显示宽).grid(row=26,column=1,sticky='W')
    Entry(全局变量展示框, textvariable=SV_TV_数据表格_表头_选中值, width=全局变量显示宽).grid(row=27,column=1,sticky='W')
    Label(全局变量展示框, text='[===END===]').grid(row=28, column=0, columnspan=2)
    ## 动态设置画布窗口宽高:根据主主窗口的参数设置限宽限高
    ##主窗口大小和位置 = top.geometry()
    ##主窗口宽, 主窗口高, 主窗口X, 主窗口Y = re.findall('[0-9]+', 主窗口大小和位置)
    总行数 = 32    # 画布最大显示行数
    画布滚动最右边 = 750
    画布滚动最下边 = 21*总行数                       # Entry 默认高度为 21像素(20像素+1分隔像素)
    DEBUG画布['scrollregion'] = (0,0,画布滚动最右边,画布滚动最下边)   # 一个元组 tuple (w, n, e, s) ,定义了画布可滚动的最大区域,w 为左边,n 为头部,e 为右边,s 为底部
    DEBUG画布['width'] = 画布滚动最右边                   # 使用实际需要的宽度即可
    DEBUG画布['height'] = min(800, 画布滚动最下边)   # 取最小值,防止纵向撑爆
    Scrollbar_画布_竖 = Scrollbar(LabelFrame_DEBUG框, command=DEBUG画布.yview)   # 创建竖滚动条
    Scrollbar_画布_竖.grid(row=0,column=1,sticky='NSEW')                  # 竖滚动条定位
    Scrollbar_画布_横 = Scrollbar(LabelFrame_DEBUG框, command=DEBUG画布.xview, orient=HORIZONTAL) # 创建横滚动条
    Scrollbar_画布_横.grid(row=1,column=0,sticky='NSEW')                                   # 横滚动条定位
    DEBUG画布.config(xscrollcommand=Scrollbar_画布_横.set, yscrollcommand=Scrollbar_画布_竖.set) # 自动设置滚动幅度
    DEBUG画布.create_window((0,0), window=DEBUG画布Frame框, anchor='nw')    # 显示画布内 DEBUG画布Frame框 控件
    #DEBUG画布.grid(row=0,column=0,sticky='nw')                         # 显示画布,不显示画布多余部分
    DEBUG画布.grid(row=0,column=0,sticky='nsew')                      # 显示画布,画布填满空间
Button_DEBUG小窗 = Button(top, text='DEBUG小窗', width=30, command=DEF_显示DEBUG小窗)
Button_DEBUG小窗.grid(row=0,column=0,sticky='E')


########################################################################################################
## 右键菜单  ###########################################################################################
###########################
## 创建新窗口 添加新记录 ##
def DEF_新增记录():
    L_字段名,L_主键 = DEF_查表字段属性返回字段名列表及主键列表()
    if L_字段名 != []:
        DEF_弹出新加记录窗口(L_字段名)
def DEF_新增记录_填入模板值():
    L_字段名,L_主键 = DEF_查表字段属性返回字段名列表及主键列表()
    if L_字段名 != []:
        D_新增数据复制值模板 = {}
        选择数据行号 = 数据框_定位行.get()
        列数量 = len(字典_查询字段_坐标_初值)
        for 列号 in range(0, 列数量):
            D_新增数据复制值模板[字典_查询字段_坐标_初值[(0,列号)]] = 字典_查询结果_坐标_初值[(选择数据行号,列号)]
        DEF_弹出新加记录窗口(L_字段名,D_新增数据复制值模板)
def 新加记录窗口_确定(窗口对象):
    Log.debug("新加记录窗口_确定") 
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        ## 查找有值的字段,拼成 INSERT SQL 语句
        L_插入字段名 = []
        L_插入字段值 = []
        字段数量 = len(字典_添加记录_坐标_对象)//2      # 添加记录框总是显示2行,第一行为字段名,第二行初始全为空,字段数量=2行总格子数的一半
        for i in range(0, 字段数量):                    # 按序号遍历每一列
            字段名 = 字典_添加记录_坐标_对象[(0,i)].get()  # 字段名都在第一行
            字段值 = 字典_添加记录_坐标_对象[(1,i)].get()  # 获取用户输入的值
            if 字段值 != '':                            # 如果用户设置的值不是空的
                L_插入字段名.append(字段名)
                L_插入字段值.append(字段值)
        if L_插入字段名 != []:                          # 不为 [] 说明有插入信息
            SQL_字段名 = ','.join(L_插入字段名)         # ['1', '2'] -> '1,2'
            SQL_字段值 = "','".join(L_插入字段值)       # ['1', '2'] -> "1','2"
            SQL_CMD = f"INSERT INTO {SQL_库名表名} ({SQL_字段名}) VALUES ('{SQL_字段值}')"    # 拼成 INSERT SQL 语句
            R = DEF_SQL_执行_成功自动提交(SQL_CMD)
            if R[0] == 0:
                执行上条查询语句_刷新显示('从头开始')   ## 成功后,更新显示表格
                窗口对象.withdraw()                     # 关闭新窗口
            else:
                tkinter.messagebox.showerror(title='ERROR', message=R[1])
        else:
            tkinter.messagebox.showerror(title='ERROR', message='请填入数据')
def 新加记录窗口_取消(窗口对象):
    Log.debug("新加记录窗口_取消")
    窗口对象.withdraw()     # 关闭新窗口
def DEF_弹出新加记录窗口(字段名列表, D_新增数据复制值模板={}):
    新窗口 = Toplevel()
    新窗口.title('添加新记录')

    ## 新窗口布局
    显编框 = Frame(新窗口)
    按钮框 = Frame(新窗口)
    显编框.grid(row=0,column=0,sticky='NWES')
    按钮框.grid(row=1,column=0)
    
    ## 宽高参数
    行数 = 2
    列数 = len(字段名列表)
    
    ## 记录字段中每个字段需要的Entry宽值
    D_已知字段宽度 = DB_INFO['字段宽度记录']
    L_字段需要宽值 = [D_已知字段宽度[i] if i in D_已知字段宽度 else 计算字符串像素宽返回Entry标准宽(i) for i in 字段名列表]
    Log.debug(f"字段名列表={字段名列表} D_已知字段宽度={D_已知字段宽度} L_字段需要宽值={L_字段需要宽值}")
    
    ## 创建画布
    画布 = Canvas(显编框, bg='#00CED1')    # 创建画布
    画布.grid(row=0,column=0)              # 显示画布
    ## 在画布里创建 Frame
    画布Frame框 = Frame(画布)
    字段框 = Frame(画布Frame框)
    字段框.grid(row=0,column=0,sticky='NW')
    数据框 = Frame(画布Frame框)
    数据框.grid(row=1,column=0,sticky='NW')
    
    ## 设置画布参数
    总行数 = 行数
    
    ## 画布可滚动显示的最大宽和高(要刚好能放下画布里的Frame里的全部控件)
    画布滚动最右边 = sum(L_字段需要宽值)*7 + 列数*4   # 总字符数量x9像素+每个单元格需要初始4像素
    画布滚动最下边 = 21*总行数     # 20*行数 + 行数*1
    
    画布['scrollregion'] = (0,0,画布滚动最右边,画布滚动最下边)   # 一个元组 tuple (w, n, e, s) ,定义了画布可滚动的最大区域,w 为左边,n 为头部,e 为右边,s 为底部
    
    ## 动态设置显示画布固定显示宽和高(要和主显示框的大小匹配)
    画布['width'] = 画布滚动最右边      # 使用实际需要的宽度即可
    画布['height'] = 画布滚动最下边
    
    Scrollbar_画布_竖 = Scrollbar(显编框, command=画布.yview)   # 竖滚动条
    Scrollbar_画布_竖.grid(row=0,column=1,sticky='NSEW')
    Scrollbar_画布_横 = Scrollbar(显编框, command=画布.xview, orient=HORIZONTAL)    # 横滚动条
    Scrollbar_画布_横.grid(row=1,column=0,sticky='NSEW')
    画布.config(xscrollcommand=Scrollbar_画布_横.set, yscrollcommand=Scrollbar_画布_竖.set) # 自动设置滚动幅度
    画布.create_window((0,0), window=画布Frame框, anchor='nw')

    ## 在 画布里的Frame里创建控件
    字典_添加记录_坐标_对象.clear()
    # 第1行是字段行,行序号固定为0
    for 列 in range(0, 列数):
        初始值 = str(字段名列表[列])
        字典_添加记录_坐标_对象[(0,列)] = Entry(字段框, width=L_字段需要宽值[列])
        字典_添加记录_坐标_对象[(0,列)].insert(0, 初始值)
        字典_添加记录_坐标_对象[(0,列)].grid(row=0,column=列,sticky='W')
        字典_添加记录_坐标_对象[(0,列)]['state'] = 'readonly'               # 设置为只读,用户不能修改
    # 第2行是数据行,行序号固定为1
    for 列 in range(0, 列数):
        字典_添加记录_坐标_对象[(1,列)] = Entry(数据框, width=L_字段需要宽值[列])
        字典_添加记录_坐标_对象[(1,列)].grid(row=1,column=列,sticky='W')
        字段名 = 字段名列表[列]
        if 字段名 in D_新增数据复制值模板:
            初始值 = D_新增数据复制值模板[字段名]
            if 初始值 != SV_自定义空值字符串表示.get():     # 非空值表示的才填入
                字典_添加记录_坐标_对象[(1,列)].insert(0, 初始值)
    
    ## 按钮框
    确定按钮 = Button(按钮框, text='确定', command=lambda 窗口对象=新窗口:新加记录窗口_确定(窗口对象))
    确定按钮.grid(row=1,column=0)
    取消按钮 = Button(按钮框, text='取消', command=lambda 窗口对象=新窗口:新加记录窗口_取消(窗口对象))
    取消按钮.grid(row=1,column=1)

    显示坐标 = '+300+200'
    新窗口.geometry(显示坐标)
    显编框.grid_columnconfigure(0, weight=1)
    新窗口.grid_columnconfigure(0, weight=1)
## 创建新窗口 添加新记录 ##
###########################

## 数据导出为CSV文件
def CSV写入本地(文件名, L_字段名, LL_数据):
    with open(文件名, mode='w', encoding='utf-8', newline='') as f:  # 以写方式打开文件。注意添加 newline='',否则会在两行数据之间都插入一行空白。
        CSV_W = csv.writer(f)
        CSV_W.writerow(L_字段名)  # 写入标题行,writerow() 一次只能写入一行
        CSV_W.writerows(LL_数据)  # 写入数据,writerows() 一次写入多行
        INFO = f'导出数据"{文件名}"成功'
        tkinter.messagebox.showinfo(title='INFO', message=INFO)
def 数据表导出CSV():
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        默认导出文件名 = f'{SQL_库名表名}.csv'
        导出文件名 = tkinter.simpledialog.askstring(title='导出文件名(重名会覆盖)', prompt='请输入导出文件名:', initialvalue=默认导出文件名)
        ## 确定为输入内容,取消为None
        if 导出文件名 != None:
            SQL_CMD = f'SELECT * FROM {SQL_库名表名}'
            R = DEF_SQL_查询和返回_数据列表_字段列表(SQL_CMD)
            if R[0] == 0:
                LL_数据 = R[1]
                L_字段名 = R[2]
                CSV写入本地(导出文件名, L_字段名, LL_数据)
            else:
                tkinter.messagebox.showerror(title='ERROR', message=R[1])
def DEF_右键菜单_此页数据导出为CSV文件():
    if LF_显示编辑框.winfo_children() == []:     # 当显示编辑框内组件不存在
        ERROR = '无法导出数据:显编框内无数据'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        默认导出文件名 = '导出部分数据.csv'
        导出文件名 = tkinter.simpledialog.askstring(title='导出文件名(重名会覆盖)', prompt='请输入导出文件名:', initialvalue=默认导出文件名)
        ## 确定为输入内容,取消为None
        if 导出文件名 != None:
            L_字段名 = [字典_查询字段_坐标_初值[K] for K in 字典_查询字段_坐标_初值]
            列数 = len(L_字段名)
            N = 0
            LL_数据 = []
            列表_数据信息 = []
            for K in 字典_查询结果_坐标_初值:
                N += 1
                列表_数据信息.append(字典_查询结果_坐标_初值[K])
                if N%列数==0:
                    LL_数据.append(列表_数据信息)
                    列表_数据信息 = []
                    N = 0
            CSV写入本地(导出文件名, L_字段名, LL_数据)
def SQL写入本地(文件名, SQL_CREATE_TABLE, SQL_INSERT):
    with open(文件名, mode='w', encoding='utf-8') as f:  # 以写方式打开文件。
        f.write(SQL_CREATE_TABLE)
        f.write(SQL_INSERT)
        INFO = f'导出数据"{文件名}"成功'
        tkinter.messagebox.showinfo(title='INFO', message=INFO)
def 数据表导出SQL():
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        默认导出文件名 = f'{SQL_库名表名}.sql'
        导出文件名 = tkinter.simpledialog.askstring(title='导出文件名(重名会覆盖)', prompt='请输入导出文件名:', initialvalue=默认导出文件名)
        ## 确定为输入内容,取消为None
        if 导出文件名 != None:
            SQL_CREATE_TABLE = 查表字段属性生成建表SQL语句()
            if SQL_CREATE_TABLE != '':
                SQL_INSERT = 查表数据内容生成批量插入SQL语句()
                if SQL_INSERT != '':
                    SQL写入本地(导出文件名, SQL_CREATE_TABLE, SQL_INSERT)
                else:
                    tkinter.messagebox.showerror(title='ERROR', message='生成写入语句失败')
            else:
                tkinter.messagebox.showerror(title='ERROR', message='生成建表语句失败')

########################################################################################################
## 右键菜单 字段框 #####################################################################################

#########################
## 创建新窗口 修改字段 ##
字典_修改字段信息对象 = {}    # 
字典_原字段信息 = {}          # {'COLUMN_DEFAULT':'', 'IS_NULLABLE':'', 'COLUMN_TYPE':'', 'COLUMN_KEY':''}
def 修改字段窗口_确定(窗口对象):
    数据表名 = SV_数据库树_选中表名.get()
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        if 数据表名 == '':
            print("没有表名")
        else:
            ## 提取缓存的字段原属性
            原字段名 = 字典_原字段信息['COLUMN_NAME']
            原默认值 = 字典_原字段信息['COLUMN_DEFAULT']
            原是否可空 = 字典_原字段信息['IS_NULLABLE']
            原字段类型 = 字典_原字段信息['COLUMN_TYPE']
            原是否主键 = 字典_原字段信息['COLUMN_KEY']
            
            字段对象列表 = 字典_修改字段信息对象['0']
            新字段名 = 字段对象列表[0].get()
            新字段类型 = 字段对象列表[1].get()
            新是否可空 = 字段对象列表[2].get()
            新默认值 = 字段对象列表[3].get()
            新是否主键 = 字段对象列表[4].get()
            
            if 原字段名==新字段名 and 原默认值==新默认值 and 原是否可空==新是否可空 and 原字段类型==新字段类型 and 原是否主键==新是否主键:
                print("没有改动,忽略")
                窗口对象.withdraw()                            # 关闭编辑窗口
            else:
                print("有改动")
                if 新默认值 != '':
                    if 新默认值 == 'auto_increment':
                        新默认值 = 'auto_increment'
                    else:
                        新默认值 = f'DEFAULT {新默认值}'
                
                ## 判断主键情况
                if 原是否主键 == '是':
                    if 新是否主键 == '否':
                        主键标识 = '不自动操作'   # 原是新否,删除主键还是手动命令操作吧 ALTER TABLE 表名 DROP PRIMARY KEY
                    else:
                        主键标识 = ''             # 原是新是,忽略
                else:
                    if 新是否主键 == '否':
                        主键标识 = ''             # 原否新否,忽略
                    else:
                        主键标识 = 'PRIMARY KEY'  # 原否新是,尝试设置主键
                
                if 主键标识 == '不自动操作':
                    WARNING = '删除主键还是手动命令操作吧'
                    tkinter.messagebox.showwarning(title='WARNING', message=WARNING)
                else:
                    if 新字段名 == 原字段名:
                        ## 修改字段属性的SQL语句
                        ## ALTER TABLE 表名 MODIFY 字段名称 字段类型 [完整性约束条件]
                        SQL_CMD = f'ALTER TABLE {SQL_库名表名} MODIFY {原字段名} {新字段类型} {新是否可空} {新默认值} {主键标识}'
                    else:
                        ## 修改字段名和字段属性
                        ## ALTER TABLE 表名 CHANGE 原字段名 新字段名 字段类型 约束条件
                        SQL_CMD = f'ALTER TABLE {SQL_库名表名} CHANGE {原字段名} {新字段名} {新字段类型} {新是否可空} {新默认值} {主键标识}'
                    print("SQL_CMD", SQL_CMD)
                    R = DEF_SQL_执行_成功自动提交(SQL_CMD)
                    print(R)
                    if R[0] == 0:                                      # 创建新字段全部成功
                        窗口对象.withdraw()                            # 关闭编辑窗口
                        执行上条查询语句_刷新显示('已翻页位置开始')    # 成功后,更新显示表格
                    else:
                        ERROR = R[1]
                        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def 修改字段窗口_取消(窗口对象):
    窗口对象.withdraw()
def DEF_弹出修改字段窗口():
    字段索引 = 字段框_定位列.get()
    原字段名 = 字典_查询字段_坐标_初值[(0,字段索引)]
    ## 查询数据库,取原字段信息
    数据表名 = SV_数据库树_选中表名.get()
    SQL_CMD = f"SELECT COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, COLUMN_TYPE, COLUMN_KEY FROM information_schema.columns WHERE table_name='{数据表名}' AND COLUMN_NAME='{原字段名}'"
    R = DEF_SQL_查询和返回(SQL_CMD)
    if R[0] == 0:
        查询结果 = R[1]
        print("查询成功,查询结果R[1]", 查询结果)    # 有值 (('A', None, 'NO', 'int(11)', 'PRI'),) 无值 ()
        if 查询结果 == ():
            ERROR = '字段属性查询结果为空,无法编辑'
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
        else:
            ## 获取字段原属性
            COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, COLUMN_TYPE, COLUMN_KEY = 查询结果[0]
            ## 缓存字段原属性
            字典_原字段信息['COLUMN_NAME'] = COLUMN_NAME
            字典_原字段信息['COLUMN_DEFAULT'] = COLUMN_DEFAULT
            if IS_NULLABLE == 'YES':
                字典_原字段信息['IS_NULLABLE'] = 'NULL'
            else:
                字典_原字段信息['IS_NULLABLE'] = 'NOT NULL'
            字典_原字段信息['COLUMN_TYPE'] = COLUMN_TYPE
            if COLUMN_KEY == 'PRI':
                字典_原字段信息['COLUMN_KEY'] = '是'
            else:
                字典_原字段信息['COLUMN_KEY'] = '否'
    
            字典_修改字段信息对象.clear()
            新窗口 = Toplevel()
            新窗口.title('修改数据字段窗口')
            显示坐标 = f'+{屏幕宽//2-300}+{屏幕高//2-100}'
            新窗口.geometry(显示坐标)
        
            表名框 = Frame(新窗口)
            标题框 = Frame(新窗口)
            数据框 = Frame(新窗口)
            按钮框 = Frame(新窗口)
            表名框.grid(row=0,column=0,sticky='NW')
            标题框.grid(row=1,column=0,sticky='NW')
            数据框.grid(row=2,column=0,sticky='NW')
            按钮框.grid(row=3,column=0)
            
            ## 标题框:固定不变的5个Entry,提示每列含义
            字段 = Entry(标题框)
            字段.grid(row=0,column=0,sticky='NW')      #00
            字段.insert(0, '列名(字段名)')
            字段['state'] = 'readonly'
            类型 = Entry(标题框, width=18)
            类型.grid(row=0,column=1,sticky='NW')      #01
            类型.insert(0, '类型')
            类型['state'] = 'readonly'
            空 = Entry(标题框, width=12)
            空.grid(row=0,column=2,sticky='NW')        #02
            空.insert(0, '是否允许空')
            空['state'] = 'readonly'
            默认值 = Entry(标题框, width=23)
            默认值.grid(row=0,column=3,sticky='NW')    #03
            默认值.insert(0, '默认值')
            默认值['state'] = 'readonly'
            主键 = Entry(标题框, width=18)
            主键.grid(row=0,column=4,sticky='NW')      #04
            主键.insert(0, '主键标识')
            主键['state'] = 'readonly'
        
            ## 数据框:查询数据库并填入原值,方便用户后续修改
            Entry_字段名 = Entry(数据框)
            Entry_字段名.insert(0, 原字段名)
            Combobox_字段类型 = ttk.Combobox(数据框, width=15)
            当前数据库类型 = SV_数据库类型.get()
            if 当前数据库类型 in ('SQLite3', 'MySQL', 'Oracle', 'MSSQL', 'PostgreSQL'):
                Combobox_字段类型['value'] = 数据字典_DB_TABLE_字段类型[当前数据库类型]
            else:
                Combobox_字段类型['value'] = 数据字典_DB_TABLE_字段类型['Other']
            Combobox_字段类型.set(COLUMN_TYPE)
            Combobox_是否可空 = ttk.Combobox(数据框, width=10)
            Combobox_是否可空['value'] = ('NULL', 'NOT NULL')
            if IS_NULLABLE == 'YES':
                Combobox_是否可空.current(0)                       # 设置默认选择值,默认值中的内容为索引,从0开始
            else:
                Combobox_是否可空.current(1)
            Combobox_默认值 = ttk.Combobox(数据框)
            Combobox_默认值['value'] = ('auto_increment', 'CURRENT_TIMESTAMP')
            if COLUMN_DEFAULT == None:
                pass
            else:
                Combobox_默认值.set(COLUMN_DEFAULT)
            
            Combobox_是否主键 = ttk.Combobox(数据框, width=15)
            Combobox_是否主键['value'] = ('是', '否')
            if COLUMN_KEY == 'PRI':
                Combobox_是否主键.current(0)
            else:
                Combobox_是否主键.current(1)
            
            字典_修改字段信息对象['0'] = [Entry_字段名, Combobox_字段类型, Combobox_是否可空, Combobox_默认值, Combobox_是否主键]
            Entry_字段名.grid(row=1,column=0,sticky='NW')
            Combobox_字段类型.grid(row=1,column=1,sticky='NW')
            Combobox_是否可空.grid(row=1,column=2,sticky='NW')
            Combobox_默认值.grid(row=1,column=3,sticky='NW')
            Combobox_是否主键.grid(row=1,column=4,sticky='NW')
        
            ## 按钮框
            确定按钮 = Button(按钮框, text='确定', command=lambda 窗口对象=新窗口:修改字段窗口_确定(窗口对象))
            确定按钮.grid(row=1,column=0)
            取消按钮 = Button(按钮框, text='取消', command=lambda 窗口对象=新窗口:修改字段窗口_取消(窗口对象))
            取消按钮.grid(row=1,column=1)
    else:
        ERROR = f'数据表"{数据表名}"的字段"{原字段名}"异常: {R[1]}'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
## 创建新窗口 修改字段 ##
#########################

def DEF_鼠标事件_右键菜单_字段单元(event):
    选中控件 = event.widget
    行 = 0                               # 字段名只有1行,恒等于0
    列 = 选中控件.grid_info()['column']
    字段框_定位列.set(列)
    ## 弹出菜单
    字段框_右键菜单.post(event.x_root, event.y_root)   # 光标位置显示菜单
def DEF_右键菜单_删除列():
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        tkinter.messagebox.showinfo(title='提示', message='SQLite3 没有这个功能,可使用新建表替换旧表')
    else:
        ## ALTER TABLE 库名.表名 DROP COLUMN 列名
        SQL_库名表名 = 生成_库名表名限定SQL语句部分()
        if SQL_库名表名 != '':
            字段显示列号 = 字段框_定位列.get()
            字段名 = 字典_查询字段_坐标_初值[(0,字段显示列号)]
            用户决定 = tkinter.messagebox.askquestion(title='请三思...', message='是否确定删除字段: '+字段名)  # 返回值为:yes/no
            if 用户决定 == 'yes':
                SQL_CMD = f'ALTER TABLE {SQL_库名表名} DROP COLUMN {字段名}'          # 删除字段的SQL语句
                R = DEF_SQL_执行_成功自动提交(SQL_CMD)
                if R[0] == 0:
                    执行上条查询语句_刷新显示('已翻页位置开始')        # 重新查询数据库表,更新显编框内容
                else:
                    tkinter.messagebox.showerror(title='ERROR', message=R[1])
            else:
                Log.debug(f"取消删除字段 {字段名}")
def DEF_右键菜单_编辑列():
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        tkinter.messagebox.showinfo(title='提示', message='SQLite3 没有这个功能,可使用新建表替换旧表')
    elif 当前数据库类型 == 'MySQL':
        DEF_弹出修改字段窗口()
    else:
        ERROR = f'{当前数据库类型} 还不会操作...'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def DEF_右键菜单_列值精确匹配():
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        if IV_急速查看模式_勾选框.get() == 1:
            字段名 = SV_TV_数据表格_表头_选中值.get()
        else:
            字段列号 = 字段框_定位列.get()
            字段名 = 字典_查询字段_坐标_初值[(0,字段列号)]
        if SV_数据库类型 == 'SQLite3':
            SQL = f"SELECT * FROM {SQL_库名表名} WHERE {字段名} IS '匹配内容'"
        else:
            SQL = f"SELECT * FROM {SQL_库名表名} WHERE {字段名} = '匹配内容'"
        DEF_按钮_清屏命令框()
        文本框_命令行.insert(0.0, SQL+'\n')
def DEF_右键菜单_列值模糊匹配():
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        if IV_急速查看模式_勾选框.get() == 1:
            字段名 = SV_TV_数据表格_表头_选中值.get()
        else:
            字段列号 = 字段框_定位列.get()
            字段名 = 字典_查询字段_坐标_初值[(0,字段列号)]
        SQL = f"SELECT * FROM {SQL_库名表名} WHERE {字段名} LIKE '%匹配内容%'"
        DEF_按钮_清屏命令框()
        文本框_命令行.insert(0.0, SQL+'\n')
def DEF_右键菜单_列值替换():
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        字段列号 = 字段框_定位列.get()
        字段名 = 字典_查询字段_坐标_初值[(0,字段列号)]
        SQL = f"UPDATE {SQL_库名表名} SET {字段名}='新值' WHERE {字段名}='原值'"
        DEF_按钮_清屏命令框()
        文本框_命令行.insert(0.0, SQL+'\n')
def DEF_右键菜单_统计本列出现次数():
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        if IV_急速查看模式_勾选框.get() == 1:
            字段名 = SV_TV_数据表格_表头_选中值.get()
        else:
            字段列号 = 字段框_定位列.get()
            字段名 = 字典_查询字段_坐标_初值[(0,字段列号)]
        SQL = f'SELECT {字段名},COUNT({字段名}) AS 出现次数 FROM {SQL_库名表名} GROUP BY {字段名} ORDER BY 出现次数 DESC'
        DEF_SQL_查询和显示(SQL)
def 显示字段信息():
    SQL_CMD = 生成_查表字段属性SQL语句('全属性展示')
    if SQL_CMD != '':
        DEF_SQL_查询和显示(SQL_CMD)

#########################################
## 右键菜单 字段框 新加字段 创建新窗口 ##
def 新加字段窗口_确定(窗口对象):
    数据表名 = SV_数据库树_选中表名.get()
    if 数据表名 == '':
        print("没有表名")
    else:
        SQL_库名表名 = 生成_库名表名限定SQL语句部分()
        if SQL_库名表名 != '':
            ## 字段名不能为空,不能重复
            错误标记 = 0
            测试字段名列表 = []
            for K in 字典_新加字段信息:
                测试字段名 = 字典_新加字段信息[K][2].get().strip()
                if 测试字段名 == '':
                    错误标记 = 1
                    print("含有空字段名")
                    break
                else:
                    测试字段名列表.append(测试字段名)
            if len(测试字段名列表) != len(set(测试字段名列表)):
                错误标记 = 1
                print("有重复字段名")
    
            if 错误标记 == 0:
                L_SQL_新增字段 = []
                for 字段编号 in 字典_新加字段信息:
                    字段对象列表 = 字典_新加字段信息[字段编号]
                    字段名 = 字段对象列表[2].get()
                    字段类型 = 字段对象列表[3].get()
                    是否可空 = 字段对象列表[4].get()
                    默认值  = 字段对象列表[5].get()
                    是否主键 = 字段对象列表[6].get()
                    if 是否主键 == '是(自增数)':
                        主键标识 = 'PRIMARY KEY AUTOINCREMENT'
                    elif 是否主键 == '是':
                        主键标识 = 'PRIMARY KEY'
                    else:
                        主键标识 = ''
                    if 默认值 != '':
                        默认值 = f'default {默认值}'
                    else:
                        默认值 == ''
                    SQL_新增字段 = f'ALTER TABLE {SQL_库名表名} ADD {字段名} {字段类型} {是否可空} {默认值} {主键标识}'
                    L_SQL_新增字段.append(SQL_新增字段)
                L_成功信息, L_失败信息 = DEF_SQL_执行多条_忽略错误语句(L_SQL_新增字段)
                if L_成功信息 != []:        # 创建新字段有成功的
                    窗口对象.withdraw()     # 关闭编辑窗口
                    执行上条查询语句_刷新显示('从头开始')
                elif L_失败信息 != []:
                    ERROR = '执行信息\n'
                    for i in L_成功信息 + L_失败信息:
                        ERROR += i+'\n'
                    tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def 新加字段窗口_取消(窗口对象):
    窗口对象.withdraw()
def 新加字段窗口_增加字段(显示框):
    行号列表 = [i for i in 字典_新加字段信息]
    if 行号列表 == []:
        新行号 = 0
    else:
        最大行号 = max(行号列表)
        新行号 = 最大行号 + 1
    
    # 字段信息编号和删除字段信息按钮
    字段编号 = Entry(显示框, width=2)
    字段编号.insert(0, 新行号)
    字段编号['state'] = 'readonly'
    字段编号.grid(row=新行号,column=1,sticky='NW')
    Button_删除本行字段信息 = Button(显示框, bitmap='error', height=15, width=15, command=lambda STR_字段编号=字段编号.get():DEF_删除新加字段按钮(STR_字段编号))
    Button_删除本行字段信息.grid(row=新行号,column=0,sticky='NW')
    # 创建5个字段属性设置对象
    Entry_字段名 = Entry(显示框)
    Combobox_字段类型 = ttk.Combobox(显示框, width=15)
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 in ('SQLite3', 'MySQL', 'Oracle', 'MSSQL', 'PostgreSQL'):
        Combobox_字段类型['value'] = 数据字典_DB_TABLE_字段类型[当前数据库类型]
    else:
        Combobox_字段类型['value'] = 数据字典_DB_TABLE_字段类型['Other']
    Combobox_是否可空 = ttk.Combobox(显示框, width=10)
    Combobox_是否可空['value'] = ('NULL', 'NOT NULL')
    Combobox_是否可空.current(0)                                             # 默认值中的内容为索引,从0开始
    Combobox_默认值 = ttk.Combobox(显示框)
    Combobox_默认值['value'] = ('datetime("now","localtime")')
    Combobox_是否主键 = ttk.Combobox(显示框, width=15)
    Combobox_是否主键['value'] = ('是(自增数)', '是', '否')
    Combobox_是否主键.current(2)
    列表_字段属性对象 = [Button_删除本行字段信息, 字段编号, Entry_字段名, Combobox_字段类型, Combobox_是否可空, Combobox_默认值, Combobox_是否主键]
    字典_新加字段信息[新行号] = 列表_字段属性对象                            # 添加到全局字典变量,KEY为行号,VALUE为组成列表依次存放的Entry对象
    # 给创建的7个控件对象设置位置
    for 列号 in range(0,7):
        列表_字段属性对象[列号].grid(row=新行号, column=列号, sticky='NW')
def DEF_删除新加字段按钮(STR_字段编号):
    print("DEF_删除新加字段按钮")
    print("STR_字段编号", STR_字段编号, type(STR_字段编号))
    KEY = int(STR_字段编号)
    for i in 字典_新加字段信息[KEY]:
        i.grid_forget()                  # 隐藏
    del 字典_新加字段信息[KEY]           # 删除字段信息
def DEF_弹出新加字段窗口():
    字典_新加字段信息.clear()     # 先清空存储新建表信息的字典
    新窗口 = Toplevel()
    新窗口.title('新增字段')
    显示坐标 = f'+{屏幕宽//2-300}+{屏幕高//2-100}'
    新窗口.geometry(显示坐标)

    提示框 = Frame(新窗口)
    标题框 = Frame(新窗口)
    数据框 = Frame(新窗口)
    按钮框1 = Frame(新窗口)
    按钮框2 = Frame(新窗口)
    提示框.grid(row=0,column=0,sticky='NW')
    标题框.grid(row=1,column=0,sticky='NW')
    数据框.grid(row=2,column=0,sticky='NW')
    按钮框1.grid(row=3,column=0,sticky='NW')
    按钮框2.grid(row=4,column=0)
    
    Label(提示框, text='[库名]').grid(row=0,column=0,sticky='NW')
    Label(提示框, text=SV_数据库树_选中库名.get(), fg='DarkOrange').grid(row=0, column=1, sticky='NW')
    Label(提示框, text='[表名]').grid(row=0,column=2,sticky='NW')
    Label(提示框, text=SV_数据库树_选中表名.get(), fg='DarkOrange').grid(row=0, column=3, sticky='NW')
    ## 标题框:固定不变的7个Entry,提示每列含义
    删除位 = Entry(标题框, width=2)
    删除位.grid(row=0,column=0,sticky='NW')    #00
    删除位.insert(0, '删')
    序号位 = Entry(标题框, width=2)
    序号位.grid(row=0,column=1,sticky='NW')    #01
    序号位.insert(0, '序')
    字段 = Entry(标题框)
    字段.grid(row=0,column=2,sticky='NW')      #02
    字段.insert(0, '列名(字段名)')
    字段['state'] = 'readonly'
    类型 = Entry(标题框, width=18)
    类型.grid(row=0,column=3,sticky='NW')      #03
    类型.insert(0, '类型')
    类型['state'] = 'readonly'
    空 = Entry(标题框, width=12)
    空.grid(row=0,column=4,sticky='NW')        #04
    空.insert(0, '是否允许空')
    空['state'] = 'readonly'
    默认值 = Entry(标题框, width=23)
    默认值.grid(row=0,column=5,sticky='NW')    #05
    默认值.insert(0, '默认值')
    默认值['state'] = 'readonly'
    主键 = Entry(标题框, width=18)
    主键.grid(row=0,column=6,sticky='NW')      #06
    主键.insert(0, '主键标识')
    主键['state'] = 'readonly'

    ## 数据框:编辑填入原值,新建填入空白
    新加字段窗口_增加字段(数据框)
    
    ## 按钮框
    确定按钮 = Button(按钮框2, text='确定', command=lambda 窗口对象=新窗口:新加字段窗口_确定(窗口对象))
    确定按钮.grid(row=1,column=0)
    取消按钮 = Button(按钮框2, text='取消', command=lambda 窗口对象=新窗口:新加字段窗口_取消(窗口对象))
    取消按钮.grid(row=1,column=1)
    增加按钮 = Button(按钮框1, text='增加字段', command=lambda Frame_控件对象=数据框:新加字段窗口_增加字段(Frame_控件对象))
    增加按钮.grid(row=1,column=2,sticky='NW')
## 右键菜单 字段框 新加字段 创建新窗口 ##
#########################################

## 创建字段框右键菜单
字段框_右键菜单 = Menu(tearoff=False)
字段框_右键菜单.add_command(label='添加数据记录', command=DEF_新增记录)  ## 菜单按钮函数:新增行(新增记录)
字段框_右键菜单.add_separator()
字段框_右键菜单.add_command(label='查看表结构(字段信息)', command=显示字段信息)  ## 菜单按钮函数:查询数据表全部字段属性并显示
字段框_右键菜单.add_command(label='统计本列出现次数', command=DEF_右键菜单_统计本列出现次数) ## 菜单按钮函数:统计本列出现次数
字段框_右键菜单.add_separator()
字段框_右键菜单.add_command(label='sql: 列值替换', command=DEF_右键菜单_列值替换)                 ## 生成替换字段内容的SQL语句
字段框_右键菜单.add_command(label='sql: 列值精确匹配 IS/=', command=DEF_右键菜单_列值精确匹配)    ## 生成精确匹配此列的SQL语句
字段框_右键菜单.add_command(label='sql: 列值模糊匹配 LIKE', command=DEF_右键菜单_列值模糊匹配)    ## 生成模糊匹配此列的SQL语句
字段框_右键菜单.add_separator()
字段框_右键菜单.add_command(label='此分页数据导出(CSV)', command=DEF_右键菜单_此页数据导出为CSV文件)
字段框_右键菜单.add_separator()
字段框_右键菜单.add_command(label='添加列(添加字段)', command=DEF_弹出新加字段窗口)   ## 菜单按钮函数:添加列(添加字段)
字段框_右键菜单.add_command(label='删除列(删除字段)', command=DEF_右键菜单_删除列)    ## 菜单按钮函数:删除列(删除字段)
字段框_右键菜单.add_command(label='编辑列(修改字段)', command=DEF_右键菜单_编辑列)    ## 菜单按钮函数:编辑列(修改字段)


########################################################################################################
## 右键菜单 数据框 #####################################################################################

###########################
## 创建新窗口 大文本编辑 ##
def 大文本窗口_确定(窗口对象):
    print("大文本编辑_确认")
    ## 获取源控件定位
    行 = 数据框_定位行.get()
    列 = 数据框_定位列.get()
    ## 提取源控件原值
    原值 = 字典_查询结果_坐标_初值[(行,列)]
    print("原值", 原值)
    ## 提取编辑后的新值
    新值 = 字典_对象存储['文本编辑对象'].get(0.0, END).rstrip('\n')      # insert 时候会多个换行,麻烦,直接删除
    现值 = 字典_查询结果_坐标_对象[(行,列)].get()                        # Entry控件是可以输入的,此处用于处理从其他值改回原值的情况
    print("用户编辑后新值", 新值)
    if 新值 != 原值:
        print("有变化")
        字典_查询结果_坐标_对象[(行,列)].delete(0, END)     # 删除原内容
        字典_查询结果_坐标_对象[(行,列)].insert(0, 新值)    # 写入新内容
        ## 改变颜色,有变化用绿色
        字典_查询结果_坐标_对象[(行,列)]['bg'] = '#7FFF00'
        ## 显示修改数据库的按钮
        修改确认框.grid(row=0,column=0)
    else:
        if 现值 != 原值:
            print("无变化,改回原值")
            字典_查询结果_坐标_对象[(行,列)].delete(0, END)     # 删除原内容
            字典_查询结果_坐标_对象[(行,列)].insert(0, 原值)    # 改回原值
        else:
            print("无变化,没有改动")
        ## 改变颜色,无变化还原白色
        字典_查询结果_坐标_对象[(行,列)]['bg'] = '#FFFFFF'

    窗口对象.withdraw()     # 关闭编辑窗口
def 大文本窗口_取消(窗口对象):
    print("取消")
    窗口对象.withdraw()
def DEF_弹出大文本窗口():
    #编辑时禁止使用分页按钮
    按钮_显编框下一页['state'] = 'disabled'        # 禁止下一页按钮
    行 = 数据框_定位行.get()
    列 = 数据框_定位列.get()
    单元格 = 字典_查询结果_坐标_对象[(行,列)]
    单元格现值 = 单元格.get()
    单元格原值 = 字典_查询结果_坐标_初值[(行,列)]
    
    新窗口 = Toplevel()
    新窗口.title('大段文本显示/编辑窗口')
    显示坐标 = f'+{屏幕宽//2-300}+{屏幕高//2-100}'
    新窗口.geometry(显示坐标)
    新窗口.grid_columnconfigure(0, weight=1)   # (第0列, 自适应窗口宽)
    新窗口.grid_rowconfigure(1, weight=1)      # (第1行, 自适应窗口高)

    小文本框 = Frame(新窗口)
    大文本框 = Frame(新窗口)
    按钮框 = Frame(新窗口)
    小文本框.grid(row=0,column=0,sticky='NW')
    大文本框.grid(row=1,column=0,sticky='NSEW')
    按钮框.grid(row=2,column=0)
    
    大文本框.grid_columnconfigure(0, weight=1)
    大文本框.grid_rowconfigure(0, weight=1)
    
    ## 小文本框
    Label(小文本框, text='[现值]').grid(row=0,column=0,sticky='W')
    Entry_原值 = Entry(小文本框, width=100)
    Entry_原值.grid(row=0,column=1,sticky='W')
    Entry_原值.insert(0, 单元格现值)
    
    Label(小文本框, text='[原值]').grid(row=1,column=0,sticky='W')
    Entry_原值 = Entry(小文本框, width=100)
    Entry_原值.grid(row=1,column=1,sticky='W')
    Entry_原值.insert(0, 单元格原值)
    
    ## 大文本框
    Text_大文本 = Text(大文本框, height=20, width=100, wrap='none')   # 不使用自动换行显示
    字典_对象存储['文本编辑对象'] = Text_大文本
    Text_大文本.insert(0.0, 单元格现值)
    Text_大文本.focus_set()                                           # 焦点移到编辑子框

    Scrollbar_编辑子框_横 = Scrollbar(大文本框, command=Text_大文本.xview, orient=HORIZONTAL)
    Scrollbar_编辑子框_竖 = Scrollbar(大文本框, command=Text_大文本.yview)
    Text_大文本.config(xscrollcommand=Scrollbar_编辑子框_横.set, yscrollcommand=Scrollbar_编辑子框_竖.set)    # 自动设置滚动条滑动幅度
    Text_大文本.grid(row=0,column=0,sticky='NSEW')
    Scrollbar_编辑子框_竖.grid(row=0, column=1, sticky='NSEW')
    Scrollbar_编辑子框_横.grid(row=1, column=0, sticky='NSEW')
    
    ## 按钮框
    确定按钮 = Button(按钮框, text='确定', command=lambda 窗口对象=新窗口:大文本窗口_确定(窗口对象))
    确定按钮.grid(row=1,column=0)
    取消按钮 = Button(按钮框, text='取消', command=lambda 窗口对象=新窗口:大文本窗口_取消(窗口对象))
    取消按钮.grid(row=1,column=1)
## 创建新窗口 大文本编辑 ##
###########################

def DEF_键盘事件_任意输入_数据单元(event):
    当前控件 = event.widget
    控件行号 = 当前控件.grid_info()['row']
    控件列号 = 当前控件.grid_info()['column']
    ## 判断内容是否有变动
    新值 = 字典_查询结果_坐标_对象[(控件行号,控件列号)].get()
    旧值 = 字典_查询结果_坐标_初值[(控件行号,控件列号)]
    if 新值 == 旧值:
        Log.debug(f"触发控件键盘事件 {(控件行号,控件列号)}:无变化 旧值={旧值}")
        当前控件['bg'] = '#FFFFFF'                            # 无变化还原白底
        按钮_显编框下一页['state'] = 'normal'                 # 解禁下一页按钮
    else:
        Log.debug(f"触发控件键盘事件 {(控件行号,控件列号)}:有变化 旧值={旧值} -> 新值={新值}")
        当前控件['bg'] = '#7FFF00'                            # 有变化改成草绿
        修改确认框.grid(row=0,column=0)                       # 显示修改数据库的按钮
        按钮_显编框下一页['state'] = 'disabled'               # 禁止下一页按钮
def DEF_鼠标事件_离开(event):
    离开控件 = event.widget
    离开行 = 离开控件.grid_info()['row']
    离开列 = 离开控件.grid_info()['column']
    ## 判断内容是否有变动
    if 数据框_定位行.get() == 离开行 and 数据框_定位列.get() == 离开列:   # 判断是触发离开事件的控件
        新值 = 字典_查询结果_坐标_对象[(离开行,离开列)].get()
        旧值 = 字典_查询结果_坐标_初值[(离开行,离开列)]
        if 新值 == 旧值:
            Log.debug(f"触发控件离开事件 {(离开行,离开列)}:无变化 旧值={旧值}")
            离开控件['bg'] = '#FFFFFF'                            # 无变化还原白底
            按钮_显编框下一页['state'] = 'normal'                 # 解禁下一页按钮
        else:
            Log.debug(f"触发控件离开事件 {(离开行,离开列)}:有变化 旧值={旧值} -> 新值={新值}")
            离开控件['bg'] = '#7FFF00'                            # 有变化改成草绿
            修改确认框.grid(row=0,column=0)                       # 显示修改数据库的按钮
            按钮_显编框下一页['state'] = 'disabled'               # 禁止下一页按钮
    else:
        Log.debug(f"触发控件离开事件 {(离开行,离开列)}:从其他控件离开(忽略)")
def DEF_鼠标事件_左键单击(event):
    选中控件 = event.widget
    行 = 选中控件.grid_info()['row']
    列 = 选中控件.grid_info()['column']
    #选中控件['bg'] = '#7FFF00'
    #字典_查询结果_坐标_对象[(行,列)].bind('<Leave>', DEF_鼠标事件_离开)  # 单击是进入编辑,给这个控件加个离开事件
    数据框_定位行.set(行)
    数据框_定位列.set(列)
def DEF_鼠标事件_右键菜单_数据单元(event):
    选中控件 = event.widget
    行 = 选中控件.grid_info()['row']
    列 = 选中控件.grid_info()['column']
    数据框_定位行.set(行)
    数据框_定位列.set(列)
    ## 右键选择的控件获得焦点
    单元格 = 字典_查询结果_坐标_对象[(行,列)]
    单元格.focus_set()                        # 焦点移到单元格
    ## 弹出菜单
    光标X轴 = event.x_root
    光标Y轴 = event.y_root
    IV_光标X轴.set(光标X轴)                   # (当前就是看看,没有实际用途)
    IV_光标Y轴.set(光标Y轴)                   # (当前就是看看,没有实际用途)
    数据框_右键菜单.post(光标X轴, 光标Y轴)    # 光标位置显示菜单
def DEF_鼠标事件_光标悬停(event):
    控件 = event.widget
    行号 = 控件.grid_info()['row']
    列号 = 控件.grid_info()['column']
    print(f"DEF_鼠标事件_光标悬停 {行号,列号}")   # 怎么实现显示提示?
def DEF_删除记录():
    控件行号 = 数据框_定位行.get()
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        R = 判断主键是否可用()
        if R[0] == 0:
            L_主键信息 = R[1]           # [(列号,主键名), (列号,主键名)]
            L_修改限定部分 = [f'{主键名} IS NULL' if 字典_查询结果_坐标_初值[(控件行号,列号)]==SV_自定义空值字符串表示.get() else f"{主键名}='{字典_查询结果_坐标_初值[(控件行号,列号)]}'" for 列号,主键名 in L_主键信息]
            SQL_WHERE_修改限定部分语句 = 'WHERE ' + ' AND '.join(L_修改限定部分)
            SQL_CMD_DELETE = f'DELETE FROM {SQL_库名表名} {SQL_WHERE_修改限定部分语句}'
            RR = DEF_SQL_执行_成功自动提交(SQL_CMD_DELETE)
            if RR[0] == 0:
                ## 操作成功后更新一下显示/编辑框
                执行上条查询语句_刷新显示('已翻页位置开始')
                ## 删除成功后的行列号和当前行列号有差别,立刻设置为无效行列号,防止后面误删
                数据框_定位行.set(-1)
                数据框_定位列.set(-1)
            else:
                tkinter.messagebox.showerror(title='ERROR', message=RR[1])
        else:
            tkinter.messagebox.showerror(title='ERROR', message=R[1])
def DEF_按钮_还原旧值():
    行号 = 数据框_定位行.get()
    列号 = 数据框_定位列.get()
    单元格_初值 = 字典_查询结果_坐标_初值[(行号,列号)]
    单元格 = 字典_查询结果_坐标_对象[(行号,列号)]
    单元格_现值 = 单元格.get()
    if 单元格_现值 != 单元格_初值:
        单元格.delete(0, END)
        单元格.insert(0, 单元格_初值)
        单元格['bg'] = '#FFFFFF'                            # 无变化还原白底
        Log.debug(f"DEF_按钮_还原旧值 (单元格_现值){单元格_现值} 还原 {单元格_初值}(单元格_初值)")
    else:
        Log.debug(f"DEF_按钮_还原旧值 (单元格_现值){单元格_现值} 相同 {单元格_初值}(单元格_初值) 忽略操作")

## 创建数据框右键菜单
数据框_右键菜单 = Menu(tearoff=False)
数据框_右键菜单.add_command(label='添加新行', command=DEF_新增记录)  ## 菜单按钮函数:添加新行
数据框_右键菜单.add_command(label='添加新行(本行内容为模板)', command=DEF_新增记录_填入模板值)  ## 菜单按钮函数:添加新行(本行内容为模板)
数据框_右键菜单.add_command(label='删除整行', command=DEF_删除记录)  ## 菜单按钮函数:删除整行(提取控件行号信息)
数据框_右键菜单.add_separator()                                      # 分隔线
数据框_右键菜单.add_command(label='大文本编辑', command=DEF_弹出大文本窗口)
数据框_右键菜单.add_separator()
数据框_右键菜单.add_command(label='还原旧值', command=DEF_按钮_还原旧值)


############################################
## TKinter 主窗口布局 - TOP框 - LF_日志框 ##
TEXT_数据库操作日志 = Text(LF_日志框, height=3, wrap='none')    # 显示改动了数据库的操作日志
Scrollbar_日志框_竖 = Scrollbar(LF_日志框)
Scrollbar_日志框_竖['command'] = TEXT_数据库操作日志.yview
Scrollbar_日志框_横 = Scrollbar(LF_日志框)
Scrollbar_日志框_横['command'] = TEXT_数据库操作日志.xview
Scrollbar_日志框_横['orient'] = HORIZONTAL
Scrollbar_日志框_竖.grid(row=0, column=1, sticky='NSEW')
Scrollbar_日志框_横.grid(row=1, column=0, sticky='NSEW')
TEXT_数据库操作日志.config(xscrollcommand=Scrollbar_日志框_横.set, yscrollcommand=Scrollbar_日志框_竖.set)  # 自动设置滚动条滑动幅度
TEXT_数据库操作日志.grid(row=0, column=0, sticky='NSEW')
TEXT_数据库操作日志.tag_config('tag_i', foreground='green')  # 自定义文本格式 绿字
TEXT_数据库操作日志.tag_config('tag_w', foreground='blue')   # 自定义文本格式 蓝字
TEXT_数据库操作日志.tag_config('tag_e', backgroun='yellow', foreground='red')  # 自定义文本格式 黄底红字

#############################################
## TKinter 主窗口布局 - TOP框 - 常用功能框 ##
def DEF_按钮_确认修改数据表():   # 修改记录:遍历全部显示内容,找出被修改的部分,组成SQL语句并执行,为避免错误修改,显示行必须有主键,根据主键修改对应行数据
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        R = 判断主键是否可用()
        if R[0] == 0:
            L_主键信息 = R[1]           # [(列号,主键名), (列号,主键名)]
            Log.debug(f'确认修改数据库 L_主键信息={L_主键信息}')
            ## 遍历数据行,找出被修改的行,生成以WHERE限定部分语句为Key的修改字典信息
            D_修改信息 = {}                                     # {'WHERE 主键1=主键1值 AND 主键2=主键2值':[(字段名,新值),(字段名,新值)]}
            for i in 字典_查询结果_坐标_对象:                   # 遍历编辑框(查询结果框)中的全部控件
                控件旧值 = 字典_查询结果_坐标_初值[i]
                控件新值 = 字典_查询结果_坐标_对象[i].get()
                if 控件旧值 != 控件新值:                        # 当原值和当前值不一致,说明此控件值被修改
                    行号,列号 = i                               # 提取当前控件的坐标
                    字段名 = 字典_查询字段_坐标_初值[(0,列号)]  # 字段名存储在字段全局变量中
                    字段值 = 控件新值
                    if 字段值 == SV_自定义空值字符串表示.get():
                        VALUE_修改部分语句 = f"{字段名}=NULL"
                    else:
                        VALUE_修改部分语句 = f"{字段名}='{字段值}'"
                    #L_修改限定部分 = []
                    #Log.debug(f"发现 被改值控件 (行号,列号)={行号,列号}")
                    #for 列号,主键名 in L_主键信息:
                    #    此行主键值 = 字典_查询结果_坐标_初值[(行号,列号)]
                    #    print(f"控件 (行号,列号)={行号,列号} (列号,主键名)={列号,主键名} 此行主键值={此行主键值} type={type(此行主键值)}")
                    #    if 此行主键值 == SV_自定义空值字符串表示.get():
                    #        修改限定部分 = f'{主键名} IS NULL'
                    #    else:
                    #        修改限定部分 = f'{主键名} = "{此行主键值}"'
                    #    L_修改限定部分.append(修改限定部分)
                    ##L_修改限定部分 = [f'{主键名} IS NULL' if 字典_查询结果_坐标_初值[(行号,列号)]==SV_自定义空值字符串表示.get() else f'{主键名}="{字典_查询结果_坐标_初值[(行号,列号)]}"' for 列号,主键名 in L_主键信息]
                    L_修改限定部分 = [f"{主键名} IS NULL" if 字典_查询结果_坐标_初值[(行号,列号)]==SV_自定义空值字符串表示.get() else f"{主键名}='{字典_查询结果_坐标_初值[(行号,列号)]}'" for 列号,主键名 in L_主键信息]
                    Log.debug(f"L_修改限定部分={L_修改限定部分}")
                    KEY_修改限定部分语句 = 'WHERE ' + ' AND '.join(L_修改限定部分)
                    ## 把同行的修改信息合并在一起,方便整合成一条修改语句
                    if KEY_修改限定部分语句 not in D_修改信息:
                        D_修改信息[KEY_修改限定部分语句] = [VALUE_修改部分语句]
                    else:
                        D_修改信息[KEY_修改限定部分语句].append(VALUE_修改部分语句)
            Log.debug(f"修改数据表(UPDATE) D_修改信息={D_修改信息}")
            
            ## 根据 D_修改信息 制作数据库语句
            L_SQL_CMD = []
            for KEY_SQL_WHERE in D_修改信息:
                合并修改部分语句 = ', '.join(D_修改信息[KEY_SQL_WHERE])
                SQL_CMD = f'UPDATE {SQL_库名表名} SET {合并修改部分语句} {KEY_SQL_WHERE}'
                L_SQL_CMD.append(SQL_CMD)
            Log.debug(f"修改数据表(UPDATE) L_SQL_CMD={L_SQL_CMD}")
            
            if L_SQL_CMD == []:
                DEBUG = '内容没有改变,不操作数据库'
                Log.debug(DEBUG)
                TEXT_数据库操作日志.insert(0.0, DEBUG+'\n', 'tag_i')
            else:
                ## 依次执行SQL语句
                L_成功信息, L_失败信息 = DEF_SQL_执行多条_忽略错误语句(L_SQL_CMD)
                ## 失败任意一条SQL语句就弹框提示
                if L_失败信息 != []:
                    SHOW_STR = ''
                    for i in L_成功信息+L_失败信息:
                        SHOW_STR += i + '\n'
                    tkinter.messagebox.showerror(title='ERROR', message=SHOW_STR)
                ## 成功任意一条语句后,更新显示表格
                if L_成功信息 != []:
                    执行上条查询语句_刷新显示('已翻页位置开始')
        else:
            tkinter.messagebox.showerror(title='ERROR', message=R[1])

    ## 按钮执行后隐藏
    修改确认框.grid_forget()     # 隐藏
def DEF_按钮_表内全字段搜索():   # 常用功能:表内全字段搜索 SELECT * FROM 库名.表名 WHERE 列名1 LIKE "%查找内容%" or 列名2 LIKE "%查找内容%";
    数据表名 = SV_数据库树_选中表名.get()
    if 数据表名 == '':
        ERROR = '请先打开一个表'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        SQL_库名表名 = 生成_库名表名限定SQL语句部分()
        if SQL_库名表名 != '':
            L_字段名,L_主键 = DEF_查表字段属性返回字段名列表及主键列表()
            if L_字段名 != []:
                查找内容 = SV_搜索内容.get().strip('\r\n').strip('\n')              # 获取查找内容值并删除末尾的回车符号
                SELECT_LIKE_TEXT = ''
                for i in range(0, len(L_字段名)):
                    if i == 0:
                        SELECT_LIKE_TEXT += f"{L_字段名[i]} LIKE '%{查找内容}%'"
                    else:
                        SELECT_LIKE_TEXT += f" or {L_字段名[i]} LIKE '%{查找内容}%'"
                SQL_CMD_SELECT_LIKE = f'SELECT * FROM {SQL_库名表名} WHERE {SELECT_LIKE_TEXT}'
                DEF_SQL_查询和显示(SQL_CMD_SELECT_LIKE)
            else:
                tkinter.messagebox.showerror(title='ERROR', message='查询字段名返回空')
修改确认框 = Frame(常用功能框)
库名表名框 = Frame(常用功能框)
搜索功能框 = Frame(常用功能框)
#修改确认框.grid(row=0,column=0)  # 占位但先不显示,在进行编辑后再显示
库名表名框.grid(row=0,column=1)
搜索功能框.grid(row=0,column=2)
按钮_确认修改数据库 = Button(修改确认框, text='确认修改', bg='#7FFF00', command=DEF_按钮_确认修改数据表)
按钮_确认修改数据库.grid(row=0, column=0, sticky='NW')
Label(库名表名框, text='[库名]:').grid(row=0, column=0)
Label(库名表名框, textvariable=SV_数据库树_选中库名, fg='DarkOrange').grid(row=0, column=1)
Label(库名表名框, text='[表名]:').grid(row=0, column=2)
Label(库名表名框, textvariable=SV_数据库树_选中表名, fg='DarkOrange').grid(row=0, column=3)
Label(搜索功能框, text='  搜索内容:').grid(row=0, column=4)
Entry(搜索功能框, textvariable=SV_搜索内容, width=15).grid(row=0,column=5)
Button(搜索功能框, text='搜索', command=DEF_按钮_表内全字段搜索).grid(row=0, column=6)
Label(搜索功能框, text='    数据显示框:').grid(row=0, column=7)
Checkbutton(搜索功能框, text='急速查看模式(快如疯狗)', variable=IV_急速查看模式_勾选框).grid(row=0,column=8)


################################################################
## TKinter 主窗口布局 - TOP框 - 左框 - Frame_数据库和表显示框 ##

###########################
## 创建新窗口 新建数据表 ##
def 键盘任意输入事件_判断表名(event):
    当前控件 = event.widget
    现值 = 当前控件.get()
    if 现值 == '':
        SV_提示信息_建表.set('请输入表名')
    else:
        SV_提示信息_建表.set('')
def 创建数据表窗口_确定(窗口对象):
    当前数据库类型 = SV_数据库类型.get()
    新建数据表_表名 = SV_新建数据表_表名.get()
    if 新建数据表_表名 == '':
        SV_提示信息_建表.set('!!!请输入表名!!!')
    else:
        SQL_库名表名 = 生成_库名表名限定SQL语句部分_建表用()
        if 当前数据库类型 == 'SQLite3':
            自增主键标记 = 0
            L_字段信息 = []
            L_主键 = []
            for 字段编号 in 字典_创建表_字段信息:
                字段对象列表 = 字典_创建表_字段信息[字段编号]
                字段名   = 字段对象列表[2].get()
                字段类型 = 字段对象列表[3].get()
                是否可空 = 字段对象列表[4].get()
                默认值   = 字段对象列表[5].get()
                是否主键 = 字段对象列表[6].get()
                if 是否主键 == '是(SQLite3自增数)':
                    自增主键标记 = 1
                    SQLite3自增主键固定格式 = f'{字段名} INTEGER PRIMARY KEY AUTOINCREMENT'
                    L_主键.append(SQLite3自增主键固定格式)
                    L_字段信息.append(SQLite3自增主键固定格式)
                else:
                    if 是否主键 == '是':
                        L_主键.append(字段名)
                    if 默认值 == '' or 默认值 == 'auto_increment':
                        pass    # 不改变
                    else:
                        默认值 = f'DEFAULT {默认值}'  # 加关键字
                    字段信息 = f'{字段名} {字段类型} {是否可空} {默认值}'
                    L_字段信息.append(字段信息)
            Log.debug(f'新建表 L_字段信息={L_字段信息}')
            if len(L_主键) == 0 or (len(L_主键) == 1 and 自增主键标记 == 1):
                SQL_CMD = f"CREATE TABLE {SQL_库名表名} ({','.join(L_字段信息)});"
            elif len(L_主键) > 0 and 自增主键标记 == 0:
                SQL_CMD = f"CREATE TABLE {SQL_库名表名} ({','.join(L_字段信息)}, primary key ({','.join(L_主键)}));"
            else:
                SQL_CMD = ''
                SV_提示信息_建表.set('!!!(多主键且含自增) 我拼不出来,自己写吧!!!')
        else:
            Log.debug(f'{SV_数据库类型.get()} 使用通用建表语句')
            L_字段信息 = []
            L_主键 = []
            for 字段编号 in 字典_创建表_字段信息:
                字段对象列表 = 字典_创建表_字段信息[字段编号]
                字段名   = 字段对象列表[2].get()
                字段类型 = 字段对象列表[3].get()
                是否可空 = 字段对象列表[4].get()
                默认值   = 字段对象列表[5].get()
                是否主键 = 字段对象列表[6].get()
                if 是否主键 == '是':
                    L_主键.append(字段名)
                
                if 默认值 in ('', 'auto_increment'):
                    pass    # 保持默认值原字符信息
                else:
                    默认值 = f'DEFAULT {默认值}'  # 加关键字
                L_字段信息.append(f'\n{字段名} {字段类型} {是否可空} {默认值}')   # 加个换行,方便查看错误行
            Log.debug(f'新建表 L_字段信息={L_字段信息}')
            if 当前数据库类型 == 'MySQL':
                新建数据表_存储引擎 = SV_新建数据表_存储引擎.get()
                新建数据表_字符集 = SV_新建数据表_字符集.get()
                if len(L_主键) == 0:
                    SQL_CMD = f"CREATE TABLE {SQL_库名表名} ({','.join(L_字段信息)}) ENGINE={新建数据表_存储引擎} DEFAULT CHARSET={新建数据表_字符集};"
                else:
                    SQL_CMD = f"CREATE TABLE {SQL_库名表名} ({','.join(L_字段信息)}, primary key ({','.join(L_主键)})) ENGINE={新建数据表_存储引擎} DEFAULT CHARSET={新建数据表_字符集};"
            else:
                if len(L_主键) == 0:
                    SQL_CMD = f"CREATE TABLE {SQL_库名表名} ({','.join(L_字段信息)})"
                else:
                    SQL_CMD = f"CREATE TABLE {SQL_库名表名} ({','.join(L_字段信息)}, primary key ({','.join(L_主键)}))"
        
        Log.debug(f'新建表 SQL_CMD={SQL_CMD}')
        if SQL_CMD != '':
            R = DEF_SQL_执行_成功自动提交(SQL_CMD)
            if R[0] == 0:               # 创建新表成功
                窗口对象.withdraw()     # 关闭编辑窗口
                DEF_数据库树_刷新库()
            else:
                tkinter.messagebox.showerror(title='ERROR', message=R[1])
def 创建数据表窗口_取消(窗口对象):
    窗口对象.withdraw()
def 创建数据表窗口_增加字段(显示框):
    print("增加字段")
    行号列表 = [i for i in 字典_创建表_字段信息]
    print("行号列表", 行号列表)
    if 行号列表 == []:
        新行号 = 0
    else:
        最大行号 = max(行号列表)
        print("最大行号", 最大行号)
        新行号 = 最大行号 + 1
    
    # 字段信息编号和删除字段信息按钮
    字段编号 = Entry(显示框, width=2)
    字段编号.insert(0, 新行号)
    字段编号['state'] = 'readonly'
    字段编号.grid(row=新行号,column=1,sticky='NW')
    Button_删除本行字段信息 = Button(显示框, bitmap='error', height=15, width=15, command=lambda STR_字段编号=字段编号.get():DEF_删除字段按钮(STR_字段编号))
    Button_删除本行字段信息.grid(row=新行号,column=0,sticky='NW')
    # 创建5个字段属性设置对象
    Entry_字段名 = Entry(显示框)
    Combobox_字段类型 = ttk.Combobox(显示框, width=15)
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 in ('SQLite3', 'MySQL', 'Oracle', 'MSSQL', 'PostgreSQL'):
        Combobox_字段类型['value'] = 数据字典_DB_TABLE_字段类型[当前数据库类型]
    else:
        Combobox_字段类型['value'] = 数据字典_DB_TABLE_字段类型['Other']
    Combobox_是否可空 = ttk.Combobox(显示框, width=10)
    Combobox_是否可空['value'] = ('NULL', 'NOT NULL')
    Combobox_是否可空.current(0)                                                # 默认值中的内容为索引,从0开始
    Combobox_默认值 = ttk.Combobox(显示框)
    Combobox_默认值['value'] = ('auto_increment', 'datetime("now","localtime")', 'CURRENT_TIMESTAMP')
    Combobox_是否主键 = ttk.Combobox(显示框, width=15)
    if SV_数据库类型.get() == 'SQLite3':
        Combobox_是否主键['value'] = ('否', '是', '是(SQLite3自增数)')
    else:
        Combobox_是否主键['value'] = ('否', '是')
    Combobox_是否主键.current(0)
    列表_字段属性对象 = [Button_删除本行字段信息, 字段编号, Entry_字段名, Combobox_字段类型, Combobox_是否可空, Combobox_默认值, Combobox_是否主键]
    字典_创建表_字段信息[新行号] = 列表_字段属性对象                            # 添加到全局字典变量,KEY为行号,VALUE为组成列表依次存放的Entry对象
    # 给创建的7个控件对象设置位置
    for 列号 in range(0,7):
        列表_字段属性对象[列号].grid(row=新行号, column=列号, sticky='NW')
def 复制数据表窗口_填入字段(显示框):
    print("填入字段")
    SQL_CMD = 生成_查表字段属性SQL语句('主要6项')
    if SQL_CMD != '':
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            LL_字段信息 = R[1]             # 示例 [(0, 'ID', 'INTEGER', 1, None, 1), (1, 'A', 'TEXT', 0, None, 0)]
            for 编号,字段名,类型,是否空,默认值,主键 in LL_字段信息:
                # 字段信息编号和删除字段信息按钮
                字段编号 = Entry(显示框, width=2)
                字段编号.insert(0, 编号)
                字段编号['state'] = 'readonly'
                字段编号.grid(row=编号,column=1,sticky='NW')
                Button_删除本行字段信息 = Button(显示框, bitmap='error', height=15, width=15, command=lambda STR_字段编号=字段编号.get():DEF_删除字段按钮(STR_字段编号))
                Button_删除本行字段信息.grid(row=编号,column=0,sticky='NW')
                # 创建5个字段属性设置对象
                Entry_字段名 = Entry(显示框)
                Entry_字段名.insert(0, 字段名)
                Combobox_字段类型 = ttk.Combobox(显示框, width=15)
                Combobox_字段类型['value'] = (类型, 'INTEGER', 'FLOAT', 'CHAR(255)', 'VARCHAR(255)', 'TEXT', 'DATE', 'timestamp', 'BLOB')
                Combobox_字段类型.current(0)
                Combobox_是否可空 = ttk.Combobox(显示框, width=10)
                Combobox_是否可空['value'] = ('NULL', 'NOT NULL')   # 刚好对应 0 NULL,1 NOT NULL
                Combobox_是否可空.current(是否空)                   # 默认值中的内容为索引,从0开始
                Combobox_默认值 = ttk.Combobox(显示框)
                Combobox_默认值['value'] = (默认值, 'datetime("now","localtime")')
                Combobox_默认值.current(0)
                Combobox_是否主键 = ttk.Combobox(显示框, width=15)
                Combobox_是否主键['value'] = ('是(自增数)', '是', '否')
                if 主键>0:
                    Combobox_是否主键.current(1)
                else:
                    Combobox_是否主键.current(2)
                列表_字段属性对象 = [Button_删除本行字段信息, 字段编号, Entry_字段名, Combobox_字段类型, Combobox_是否可空, Combobox_默认值, Combobox_是否主键]
                字典_创建表_字段信息[编号] = 列表_字段属性对象                            # 添加到全局字典变量,KEY为行号,VALUE为组成列表依次存放的Entry对象
                # 给创建的7个控件对象设置位置
                for 列号 in range(0,7):
                    列表_字段属性对象[列号].grid(row=编号, column=列号, sticky='NW')
        else:
            tkinter.messagebox.showerror(title='ERROR', message=R[1])
def DEF_删除字段按钮(STR_字段编号):
    print("DEF_删除字段按钮")
    print("STR_字段编号", STR_字段编号, type(STR_字段编号))
    KEY = int(STR_字段编号)
    for i in 字典_创建表_字段信息[KEY]:
        i.grid_forget()                     # 隐藏
    del 字典_创建表_字段信息[KEY]           # 删除字段信息
def DEF_弹出创建数据表窗口(新建or复制):
    字典_创建表_字段信息.clear()     # 先清空存储新建表信息的字典
    新窗口 = Toplevel()
    新窗口.title('创建数据表窗口')
    显示坐标 = f'+{屏幕宽//2-300}+{屏幕高//2-100}'
    新窗口.geometry(显示坐标)

    表名框 = Frame(新窗口)
    标题框 = Frame(新窗口)
    数据框 = Frame(新窗口)
    按钮框1 = Frame(新窗口)
    按钮框2 = Frame(新窗口)
    表名框.grid(row=0,column=0,sticky='NW')
    标题框.grid(row=1,column=0,sticky='NW')
    数据框.grid(row=2,column=0,sticky='NW')
    按钮框1.grid(row=3,column=0,sticky='NW')
    按钮框2.grid(row=4,column=0)
    
    ## 表名框:用户输入新建的表名
    if SV_数据库类型.get() == 'MySQL':
        def Combobox_数据库引擎选择后执行函数(event):
            SV_新建数据表_存储引擎.set(Combobox_数据库引擎.get())
        def Combobox_字符集选择后执行函数(event):
            SV_新建数据表_字符集.set(Combobox_字符集.get())
        Label(表名框, text='[库名]').grid(row=0,column=0,sticky='NW')
        Entry(表名框, textvariable=SV_新建数据表_库名).grid(row=0,column=1,sticky='NW')
        Label(表名框, text='[存储引擎]').grid(row=1,column=0,sticky='NW')
        Combobox_数据库引擎 = ttk.Combobox(表名框, width=10)
        Combobox_数据库引擎['value'] = ('InnoDB', 'MyISAM')
        Combobox_数据库引擎.current(0)                           # 默认值中的内容为索引,从0开始
        Combobox_数据库引擎.grid(row=1, column=1, sticky='NW')
        SV_新建数据表_存储引擎.set(Combobox_数据库引擎.get())            # 设置全局变量为框内值
        Combobox_数据库引擎.bind('<<ComboboxSelected>>', Combobox_数据库引擎选择后执行函数)
        Label(表名框, text='[字符集]').grid(row=2,column=0,sticky='NW')
        Combobox_字符集 = ttk.Combobox(表名框, width=10)
        Combobox_字符集['value'] = ('UTF8', 'BIG5', 'GB2312', 'ASCII')     # 查询支持的字符集 show character set;
        Combobox_字符集.current(0)
        Combobox_字符集.grid(row=2, column=1, sticky='NW')
        SV_新建数据表_字符集.set(Combobox_字符集.get())
        Combobox_字符集.bind('<<ComboboxSelected>>', Combobox_字符集选择后执行函数)
        
    SV_新建数据表_库名.set(SV_数据库树_选中库名.get())
    SV_新建数据表_表名.set('')
    Label(表名框, text='[表名]').grid(row=0,column=2,sticky='NW')
    Entry_新表名 = Entry(表名框, textvariable=SV_新建数据表_表名)
    Entry_新表名.bind('<KeyRelease>', 键盘任意输入事件_判断表名)       # 每当输入内容时执行一次函数
    Entry_新表名.grid(row=0, column=3, sticky='NW')
    SV_提示信息_建表.set('请输入新表名')
    Label_提示信息_建表 = Label(表名框, textvariable=SV_提示信息_建表)
    Label_提示信息_建表['fg'] = 'red'
    Label_提示信息_建表.grid(row=0,column=4,sticky='NW')
    
    ## 标题框:固定不变的5个Entry,提示每列含义
    删除位 = Entry(标题框, width=2)
    删除位.grid(row=0,column=0,sticky='NW')    #00
    删除位.insert(0, '删')
    序号位 = Entry(标题框, width=2)
    序号位.grid(row=0,column=1,sticky='NW')    #01
    序号位.insert(0, '序')
    字段 = Entry(标题框)
    字段.grid(row=0,column=2,sticky='NW')      #02
    字段.insert(0, '列名(字段名)')
    字段['state'] = 'readonly'
    类型 = Entry(标题框, width=18)
    类型.grid(row=0,column=3,sticky='NW')      #03
    类型.insert(0, '类型')
    类型['state'] = 'readonly'
    空 = Entry(标题框, width=12)
    空.grid(row=0,column=4,sticky='NW')        #04
    空.insert(0, '是否允许空')
    空['state'] = 'readonly'
    默认值 = Entry(标题框, width=23)
    默认值.grid(row=0,column=5,sticky='NW')    #05
    默认值.insert(0, '默认值')
    默认值['state'] = 'readonly'
    主键 = Entry(标题框, width=18)
    主键.grid(row=0,column=6,sticky='NW')      #06
    主键.insert(0, '主键标识')
    主键['state'] = 'readonly'

    ## 数据框:编辑填入原值,新建填入空白
    if 新建or复制 == '新建':
        创建数据表窗口_增加字段(数据框)
    else:
        复制数据表窗口_填入字段(数据框)
    
    ## 按钮框
    增加按钮 = Button(按钮框1, text='增加字段', command=lambda Frame_控件对象=数据框:创建数据表窗口_增加字段(Frame_控件对象))
    增加按钮.grid(row=0,column=0,sticky='NW',columnspan=2)
    确定按钮 = Button(按钮框2, text='确定', command=lambda 窗口对象=新窗口:创建数据表窗口_确定(窗口对象))
    确定按钮.grid(row=1,column=0)
    取消按钮 = Button(按钮框2, text='取消', command=lambda 窗口对象=新窗口:创建数据表窗口_取消(窗口对象))
    取消按钮.grid(row=1,column=1)
## 创建新窗口 新建数据表 ##
###########################

def TV在指定节点添加子项(上级IID, 本级text, 类型, 上级text):
    本级IID = Treeview_数据库结构.insert(上级IID, END, text=本级text, values=[类型, 上级IID, 上级text])
    print(f"  本级IID={本级IID}  上级IID={上级IID}  OBJ: {本级IID} = Treeview_数据库结构.insert({上级IID}, END, text={本级text}, values={[类型, 上级IID, 上级text]})")
def DEF_数据库树_鼠标左键单击(event): # DEBUG用,鼠标左键点击选项事件
    sels = event.widget.selection()      # event.widget获取Treeview对象,调用selection获取选择对象名称
    print(f"DEF_数据库树_鼠标左键单击【查看对象】sels={sels}")
    for idx in sels:
        print(f"  idx={idx} Treeview_数据库结构={Treeview_数据库结构} Treeview_数据库结构.item(idx)={Treeview_数据库结构.item(idx)}")
        本级text = Treeview_数据库结构.item(idx)['text']
        库或表,上级IID,上级text = Treeview_数据库结构.item(idx)['values']  # ['库', 'TOP'] 或 ['表', '上级库名']
        print(f"  本级text={本级text} 库或表={库或表} 上级IID={上级IID} 上级text={上级text}")
def DEF_数据库树_鼠标右键(event):
    选中IID = Treeview_数据库结构.identify_row(event.y)
    print(f"DEF_数据库树_鼠标右键 选中IID={选中IID}")
    if 选中IID:
        Treeview_数据库结构.selection_set(选中IID)
        选中_TV_IID.set(选中IID)
        本级text = Treeview_数据库结构.item(选中IID)['text']
        库或表,上级IID,上级text = Treeview_数据库结构.item(选中IID)['values']  # ['库', 'I001', 'TOP'] 或 ['表', 'I002', '上级显示text']
        print(f"  本级text={本级text} 库或表={库或表} 上级text={上级text}")
        if 库或表 == '库':
            SV_数据库树_选中库名.set(本级text)
            数据库_右键菜单.post(event.x_root, event.y_root)     # 光标位置显示菜单
        elif 库或表 == '表':
            SV_数据库树_选中表名.set(本级text)
            SV_数据库树_选中库名.set(上级text)
            数据表_右键菜单.post(event.x_root, event.y_root)
    else:
        print("  【点在树外】")
        数据表_右键菜单_空白处.post(event.x_root, event.y_root)
def DEF_数据库树_鼠标左键双击(event):
    选中IID = Treeview_数据库结构.identify_row(event.y)
    Log.debug(f"DEF_数据库树_鼠标左键双击 选中IID={选中IID}")
    if 选中IID:
        Treeview_数据库结构.selection_set(选中IID)
        本级text = Treeview_数据库结构.item(选中IID)['text']
        库或表,上级IID,上级text = Treeview_数据库结构.item(选中IID)['values'] # ['库', 'TOP'] 或 ['表', '上级库名']
        print(f"  本级text={本级text} 库或表={库或表} 上级text={上级text}")
        if 库或表 == '库':
            SV_数据库树_选中库名.set(本级text)
            if Treeview_数据库结构.item(选中IID)['open'] == 0:
                print(f"打开库: {本级text} 【功能暂停使用】")
                # DEF_数据库树_打开库() # 暂停使用
        elif 库或表 == '表':
            SV_数据库树_选中表名.set(本级text)
            SV_数据库树_选中库名.set(上级text)
            if Treeview_数据库结构.item(选中IID)['open'] == 0:
                print(f"打开表: {上级text}.{本级text}")
                DEF_数据库树_打开表()
    else:
        print("  【点在树外】")

Treeview_数据库结构 = ttk.Treeview(Frame_数据库和表显示框, columns=('id', 'name'), show='tree', displaycolumns=(), height=20)
#Treeview_数据库结构.bind('<<TreeviewSelect>>', DEF_数据库树_鼠标左键单击)  # DEBUG用,鼠标左键点击选项事件
Treeview_数据库结构.bind('<Double-Button-1>', DEF_数据库树_鼠标左键双击)   # 打开库或表
Treeview_数据库结构.bind('<Button-3>', DEF_数据库树_鼠标右键)              # 打开库菜单或表菜单

Scrollbar_数据库和表框_横 = Scrollbar(Frame_数据库和表显示框, orient=HORIZONTAL, command=Treeview_数据库结构.xview)
Scrollbar_数据库和表框_竖 = Scrollbar(Frame_数据库和表显示框, orient=VERTICAL,   command=Treeview_数据库结构.yview)
Scrollbar_数据库和表框_竖.grid(row=0, column=1, sticky='NSEW')
Scrollbar_数据库和表框_横.grid(row=1, column=0, sticky='NSEW')
Treeview_数据库结构.config(xscrollcommand=Scrollbar_数据库和表框_横.set, yscrollcommand=Scrollbar_数据库和表框_竖.set)  # 自动设置滚动条滑动幅度
Treeview_数据库结构.grid(row=0,column=0,sticky='NSEW')

def DEF_数据库树_刷新库():
    选中IID = 选中_TV_IID.get()
    类型, 上级IID, 上级text = Treeview_数据库结构.item(选中IID)['values']
    if 类型 == '表':
        选中_TV_IID.set(上级IID)    # 定位到上级库名
    DEF_数据库树_打开库()
def DEF_数据库树_打开库():     # 查询并显示库内数据表名
    选中IID = 选中_TV_IID.get()
    items = Treeview_数据库结构.get_children(选中IID)
    Log.debug(f"刷新 先清理旧内容 选中IID={选中IID} 子IID={items}")
    [Treeview_数据库结构.delete(item) for item in items]
    
    Treeview_数据库结构.selection_set(选中IID)
    库级text = Treeview_数据库结构.item(选中IID)['text']
    
    SQL_CMD = 生成_查表SQL语句()
    if SQL_CMD != '':
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            查询结果 = R[1]
            if len(查询结果) == 0:
                SV_数据表列表.set('[]')
                tkinter.messagebox.showinfo(title='INFO', message='无数据表')
            else:
                L_数据表名 = [i[0] for i in 查询结果]
                SV_数据表列表.set(str(L_数据表名))
                for DB_表名 in L_数据表名:
                    TV在指定节点添加子项(选中IID, DB_表名, '表', 库级text) ## 上级IID, 本级text, 类型, 上级text
                DEF_数据库树_展开库()
        else:
            tkinter.messagebox.showerror(title='ERROR', message=R[1])
def DEF_数据库树_新建表():
    DEF_弹出创建数据表窗口('新建')
def DEF_数据库树_展开库():
    选中IID = 选中_TV_IID.get()
    try:
        if Treeview_数据库结构.item(选中IID)['open'] == 0:    # 判断未展开
            Treeview_数据库结构.item(选中IID, open=True)      # 设置展开
            print(f"展开库 {选中IID} 完成")
    except Exception as e:
        print(f"展开库 {选中IID} 失败 {e}")
def DEF_数据库树_折叠库():
    选中IID = 选中_TV_IID.get()
    try:
        if Treeview_数据库结构.item(选中IID)['open'] != 0:    # 判断已展开
            Treeview_数据库结构.item(选中IID, open=0)         # 设置折叠
            print(f"折叠库 {选中IID} 完成")
    except Exception as e:
        print(f"折叠库 {选中IID} 失败 {e}")
def DEF_右键菜单_刷新全部库():
    items = Treeview_数据库结构.get_children()
    [Treeview_数据库结构.delete(item) for item in items]
    DEF_DB_查库_填入数据库树()
数据库_右键菜单 = Menu(tearoff=False)
数据库_右键菜单.add_command(label='打开/刷新 数据库', command=DEF_数据库树_打开库)
数据库_右键菜单.add_separator()
数据库_右键菜单.add_command(label='新建表', command=DEF_数据库树_新建表)  ## 菜单按钮函数:新建数据库表
数据库_右键菜单.add_separator()
数据库_右键菜单.add_command(label='展开库', command=DEF_数据库树_展开库)
数据库_右键菜单.add_command(label='折叠库', command=DEF_数据库树_折叠库)
数据库_右键菜单.add_separator()
数据库_右键菜单.add_command(label='刷新全部库', command=DEF_右键菜单_刷新全部库)
#数据库_右键菜单.add_command(label='导出全库(CSV)', command=数据库导出CSV) ## 菜单按钮函数:导出数据库中每个表为CSV格式文件

def DEF_数据库树_打开表():     # 打开数据表(查询表内容)
    SQL_CMD = 生成_查表内容SQL语句()
    if SQL_CMD != '':
        DEF_SQL_查询和显示(SQL_CMD)
def DEF_右键菜单_删除表():
    选中IID = 选中_TV_IID.get()
    类型, 上级IID, 上级text = Treeview_数据库结构.item(选中IID)['values']
    
    SQL_库名表名 = 生成_库名表名限定SQL语句部分()
    if SQL_库名表名 != '':
        用户决定 = tkinter.messagebox.askquestion(title='请三思...', message='是否确定删除数据表: '+SQL_库名表名)  # 返回值为:yes/no
        if 用户决定 == 'yes':
            SQL_CMD = f'DROP TABLE {SQL_库名表名}'          # 删除表的SQL语句
            R = DEF_SQL_执行_成功自动提交(SQL_CMD)
            if R[0] == 0:
                DEF_数据库树_刷新库()
            else:
                tkinter.messagebox.showerror(title='ERROR', message=R[1])
def DEF_右键菜单_复制表():
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        DEF_弹出创建数据表窗口('复制')
    elif 当前数据库类型 == 'MySQL':
        tkinter.messagebox.showerror(title='ERROR', message='施工中')
    else:
        ERROR = f'{当前数据库类型} 没有做'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def DEF_右键菜单_查看表结构():
    显示字段信息()
数据表_右键菜单 = Menu(tearoff=False)                                     ## 创建数据表右键菜单
数据表_右键菜单.add_command(label='打开表', command=DEF_数据库树_打开表)  ## 菜单按钮函数:打开数据库表
数据表_右键菜单.add_command(label='删除表', command=DEF_右键菜单_删除表)  ## 菜单按钮函数:删除数据库表
数据表_右键菜单.add_command(label='新建表', command=DEF_数据库树_新建表)  ## 菜单按钮函数:新建数据库表
数据表_右键菜单.add_command(label='复制表', command=DEF_右键菜单_复制表)  ## 菜单按钮函数:复制数据库表
数据表_右键菜单.add_separator()
数据表_右键菜单.add_command(label='查看表结构(字段信息)', command=DEF_右键菜单_查看表结构)  ## 菜单按钮函数:查看表结构
数据表_右键菜单.add_separator()
数据表_右键菜单.add_command(label='导出此表(CSV)', command=数据表导出CSV) ## 菜单按钮函数:导出选中的数据库表为CSV格式文件
数据表_右键菜单.add_command(label='导出此表(SQL)', command=数据表导出SQL) ## 菜单按钮函数:导出选中的数据库表为SQL语句文件
数据表_右键菜单_空白处 = Menu(tearoff=False)      ## 创建数据表右键菜单(空白处)
数据表_右键菜单_空白处.add_command(label='空白处')

##################################################
## TKinter 主窗口布局 - TOP框 - LF_数据库信息框 ##
SV_安全模式状态 = StringVar()
def DEF_DB_查库_填入数据库树():
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'SQLite3':
        上级text = 'ROOT'
        上级IID = ''
        本级text = 'SQLite'
        类型 = '根'
        顶级IID = Treeview_数据库结构.insert(上级IID, END, text=本级text, values=[类型, 上级IID, 上级text])      # 在根节点插入子节点(作为顶级顶点) # 指定插入位置,0表示在头部插入,END表示在尾部插入。
        print(f"  顶级IID={顶级IID}  上级IID={上级IID}  OBJ: {顶级IID} = Treeview_数据库结构.insert({上级IID}, END, text={本级text}, values={[类型, 上级IID, 上级text]})")
        Treeview_数据库结构.item(顶级IID, open=True)             ## 设置展开
        
        上级text = 'SQLite'
        上级IID = 顶级IID
        本级text = 'SQLite3'
        类型 = '库'
        本级IID = Treeview_数据库结构.insert(上级IID, END, text=本级text, values=[类型, 上级IID, 上级text])
        print(f"  本级IID={本级IID}  上级IID={上级IID}  OBJ: {本级IID} = Treeview_数据库结构.insert({上级IID}, END, text={本级text}, values={[类型, 上级IID, 上级text]})")
        Treeview_数据库结构.item(顶级IID, open=True)             ## 设置展开
        
        选中_TV_IID.set(本级IID)
        DEF_数据库树_打开库()
        SV_数据库树_选中库名.set('SQLite3')
    elif 当前数据库类型 == 'MySQL':
        SQL_CMD = 'SHOW DATABASES;'
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            上级text = 'MySQL'
            顶级IID = Treeview_数据库结构.insert('', END, text=上级text, values=['根', '', 'ROOT'])  # 在根节点插入子节点(作为顶级顶点)
            Treeview_数据库结构.item(顶级IID, open=True)         ## 设置展开
            Log.debug(f"DEF_MySQL_查库 {R[1]}")
            查询结果 = R[1]
            if 查询结果 == []:
                DB_INFO['数据库列表'] = []
                SV_数据库列表.set('[]')
            else:
                L_数据库名 = [i[0] for i in 查询结果]
                DB_INFO['数据库列表'] = L_数据库名
                SV_数据库列表.set(str(L_数据库名))
                for DB_库名 in L_数据库名:
                    TV在指定节点添加子项(顶级IID, DB_库名, '库', 上级text) ## 上级IID, 本级text, 类型, 上级text
                
                ## 连接信息中指定了库名,且库是存在的,自动打开库
                设置库名 = SV_登录库名.get()
                if 设置库名 in L_数据库名:
                    L_TV_IID = Treeview_数据库结构.get_children(顶级IID)
                    for TV_IID in L_TV_IID:
                        if Treeview_数据库结构.item(TV_IID)['text'] == 设置库名:
                            SV_数据库树_选中库名.set(设置库名)
                            选中_TV_IID.set(TV_IID)
                            DEF_数据库树_打开库()
        else:
            tkinter.messagebox.showerror(title='ERROR', message=R[1])
    elif 当前数据库类型 == 'Oracle':
        上级text = 'ROOT'
        上级IID = ''
        本级text = 'Oracle'
        类型 = '根'
        顶级IID = Treeview_数据库结构.insert(上级IID, END, text=本级text, values=[类型, 上级IID, 上级text])      # 在根节点插入子节点(作为顶级顶点) # 指定插入位置,0表示在头部插入,END表示在尾部插入。
        print(f"  顶级IID={顶级IID}  上级IID={上级IID}  OBJ: {顶级IID} = Treeview_数据库结构.insert({上级IID}, END, text={本级text}, values={[类型, 上级IID, 上级text]})")
        Treeview_数据库结构.item(顶级IID, open=True)             ## 设置展开
        
        上级text = 'Oracle'
        上级IID = 顶级IID
        本级text = SV_登录帐号.get()
        类型 = '库'
        本级IID = Treeview_数据库结构.insert(上级IID, END, text=本级text, values=[类型, 上级IID, 上级text])
        print(f"  本级IID={本级IID}  上级IID={上级IID}  OBJ: {本级IID} = Treeview_数据库结构.insert({上级IID}, END, text={本级text}, values={[类型, 上级IID, 上级text]})")
        Treeview_数据库结构.item(顶级IID, open=True)             ## 设置展开
        
        选中_TV_IID.set(本级IID)
        DEF_数据库树_打开库()
        SV_数据库树_选中库名.set(SV_登录帐号.get())
    elif 当前数据库类型 == 'MSSQL':
        SQL_CMD = 'SELECT name FROM master..sysdatabases'
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            上级text = 'MSSQL'
            顶级IID = Treeview_数据库结构.insert('', END, text=上级text, values=['根', '', 'ROOT'])  # 在根节点插入子节点(作为顶级顶点)
            Treeview_数据库结构.item(顶级IID, open=True)         ## 设置展开
            Log.debug(f"MSSQL查库 {R[1]}")
            查询结果 = R[1]
            if 查询结果 == []:
                DB_INFO['数据库列表'] = []
                SV_数据库列表.set('[]')
            else:
                L_数据库名 = [i[0] for i in 查询结果]
                DB_INFO['数据库列表'] = L_数据库名
                SV_数据库列表.set(str(L_数据库名))
                for DB_库名 in L_数据库名:
                    TV在指定节点添加子项(顶级IID, DB_库名, '库', 上级text) ## 上级IID, 本级text, 类型, 上级text
                
                ## 连接信息中指定了库名,且库是存在的,自动打开库
                设置库名 = SV_登录库名.get()
                if 设置库名 in L_数据库名:
                    L_TV_IID = Treeview_数据库结构.get_children(顶级IID)
                    for TV_IID in L_TV_IID:
                        if Treeview_数据库结构.item(TV_IID)['text'] == 设置库名:
                            SV_数据库树_选中库名.set(设置库名)
                            选中_TV_IID.set(TV_IID)
                            DEF_数据库树_打开库()
        else:
            tkinter.messagebox.showerror(title='ERROR', message=R[1])
    elif 当前数据库类型 == 'PostgreSQL':
        SQL_CMD = 'SELECT schema_name FROM information_schema.schemata'
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            上级text = 'PostgreSQL'
            顶级IID = Treeview_数据库结构.insert('', END, text=上级text, values=['根', '', 'ROOT'])  # 在根节点插入子节点(作为顶级顶点)
            Treeview_数据库结构.item(顶级IID, open=True)         ## 设置展开
            Log.debug(f"DEF_PostgreSQL_查库 {R[1]}")
            查询结果 = R[1]
            if 查询结果 == []:
                DB_INFO['数据库列表'] = []
                SV_数据库列表.set('[]')
            else:
                L_数据库名 = [i[0] for i in 查询结果]
                DB_INFO['数据库列表'] = L_数据库名
                SV_数据库列表.set(str(L_数据库名))
                for DB_库名 in L_数据库名:
                    TV在指定节点添加子项(顶级IID, DB_库名, '库', 上级text) ## 上级IID, 本级text, 类型, 上级text
                
                ## 连接信息中指定了库名,且库是存在的,自动打开库
                设置库名 = SV_登录库名.get()
                if 设置库名 in L_数据库名:
                    L_TV_IID = Treeview_数据库结构.get_children(顶级IID)
                    for TV_IID in L_TV_IID:
                        if Treeview_数据库结构.item(TV_IID)['text'] == 设置库名:
                            SV_数据库树_选中库名.set(设置库名)
                            选中_TV_IID.set(TV_IID)
                            DEF_数据库树_打开库()
        else:
            tkinter.messagebox.showerror(title='ERROR', message=R[1])
    else:
        print(f"DEF_DB_查库_填入数据库树 未知数据库类型: {当前数据库类型}")
def 选择SQLite3数据库文件():
    数据库文件 = filedialog.askopenfilename()
    SV_SQLite3_数据库路径.set(数据库文件)              # 实时更新显示
def 选择连接配置文件():
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'MySQL':
        MySQL连接配置文件路径 = filedialog.askopenfilename()
        SV_MySQL_CONF_PATH.set(MySQL连接配置文件路径)       # 及时更新
    elif 当前数据库类型 == 'MSSQL':
        MSSQL连接配置文件路径 = filedialog.askopenfilename()
        SV_MSSQL_CONF_PATH.set(MSSQL连接配置文件路径)
    elif 当前数据库类型 == 'Oracle':
        Oracle连接配置文件路径 = filedialog.askopenfilename()
        SV_Oracle_CONF_PATH.set(Oracle连接配置文件路径)
    elif 当前数据库类型 == 'PostgreSQL':
        PostgreSQL连接配置文件路径 = filedialog.askopenfilename()
        SV_PostgreSQL_CONF_PATH.set(PostgreSQL连接配置文件路径)
def 读取连接配置文件自动设置登录参数(FILE_CONF_PATH):
    if FILE_CONF_PATH.strip() != '':                 # 截掉头尾的空格后不为空
        try:
            f = open(FILE_CONF_PATH, 'r')
            FILE_CONF_TEXT = f.read()                # 读取全部内容,保存到变量
            f.close()
            SP = FILE_CONF_TEXT.split(',')
            填补参数数量 = 6-len(SP)
            if 填补参数数量 > 0:
                SP = [i.strip() for i in SP] + ['' for i in range(0,填补参数数量)]
            SV_登录帐号.set(SP[0].strip())
            SV_登录密码.set(SP[1].strip())
            SV_登录地址.set(SP[2].strip())
            SV_服务端口.set(SP[3].strip())
            SV_登录库名.set(SP[4].strip())
            SV_字符集.set(SP[5].strip())
        except Exception as e:
            ERROR = f"{e}\n 格式: 账号,密码,地址,端口,库名,字符集"
            tkinter.messagebox.showerror(title='ERROR', message=str(e))
        else:
            Log.debug(f"载入连接配置文件 {FILE_CONF_PATH} 成功")
def DEF_按钮_载入连接配置():
    当前数据库类型 = SV_数据库类型.get()
    if 当前数据库类型 == 'MySQL':
        FILE_MySQL_CONF = SV_MySQL_CONF_PATH.get()
        if FILE_MySQL_CONF.strip() == '':                 # 截掉头尾的空格后为空
            选择连接配置文件()
            FILE_MySQL_CONF = SV_MySQL_CONF_PATH.get()
        读取连接配置文件自动设置登录参数(FILE_MySQL_CONF)
    elif 当前数据库类型 == 'MSSQL':
        FILE_MSSQL_CONF = SV_MSSQL_CONF_PATH.get()
        if FILE_MSSQL_CONF.strip() == '':                 # 截掉头尾的空格后为空
            选择连接配置文件()
            FILE_MSSQL_CONF = SV_MSSQL_CONF_PATH.get()
        读取连接配置文件自动设置登录参数(FILE_MSSQL_CONF)
    elif 当前数据库类型 == 'Oracle':
        FILE_Oracle_CONF = SV_Oracle_CONF_PATH.get()
        if FILE_Oracle_CONF.strip() == '':                 # 截掉头尾的空格后为空
            选择连接配置文件()
            FILE_Oracle_CONF = SV_Oracle_CONF_PATH.get()
        读取连接配置文件自动设置登录参数(FILE_Oracle_CONF)
    elif 当前数据库类型 == 'PostgreSQL':
        FILE_PostgreSQL_CONF = SV_PostgreSQL_CONF_PATH.get()
        if FILE_PostgreSQL_CONF.strip() == '':                 # 截掉头尾的空格后为空
            选择连接配置文件()
            FILE_PostgreSQL_CONF = SV_PostgreSQL_CONF_PATH.get()
        读取连接配置文件自动设置登录参数(FILE_PostgreSQL_CONF)
    else:
        ERROR = f'未知{当前数据库类型}类型数据库'
        TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
def DEF_切换模式前置操作():
    FRAME_CLEAR(LF_数据库信息框)  # 清空控件
    Button_使用SQLite.config(bg='#FFFFFF')
    Button_使用MySQL.config(bg='#FFFFFF')
    Button_使用Oracle.config(bg='#FFFFFF')
    Button_使用MSSQL.config(bg='#FFFFFF')
    Button_使用PostgreSQL.config(bg='#FFFFFF')
    SV_登录帐号.set('')
    SV_登录密码.set('')
    SV_登录地址.set('')
    SV_服务端口.set('')
    SV_登录库名.set('')
    SV_字符集.set('')
    DB_INFO['最后查询语句'] = ''
    SV_最后查询语句.set('')
def 连接MySQL数据库():
    登录地址 = SV_登录地址.get()
    服务端口 = SV_服务端口.get()
    if 服务端口 == '':
        服务端口 = 3306             # MySQL 端口要int类型
    else:
        try:
            服务端口 = int(服务端口)
        except Exception as e:
            ERROR = f'服务端口="{服务端口}" 非数字内容'
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
        else:
            登录帐号 = SV_登录帐号.get()
            登录密码 = SV_登录密码.get()
            登录库名 = SV_登录库名.get()
            字符集 = SV_字符集.get()
            if 字符集.strip() == '':
                字符集 = 'utf8'
            R = DEV_MySQL_OPEN(登录地址, 登录帐号, 登录密码, 服务端口, 登录库名, 字符集)
            if R[0] == 0:
                DB_INFO['数据库连接对象'] = R[1]      # 同步全局字典:保存数据库连接对象
                SV_数据库连接对象.set(str(R[1]))
                DEF_DB_查库_填入数据库树()
                SV_数据库树_选中库名.set(登录库名)
                TEXT_数据库操作日志.insert(0.0, 'MySQL 连接成功\n', 'tag_i')
            else:
                ERROR = f'MySQL 连接失败 {R[1]}'
                TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
                tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def 连接MSSQL数据库():
    登录地址 = SV_登录地址.get()
    服务端口 = SV_服务端口.get()
    if 服务端口 == '':
        服务端口 = '1433'
    登录帐号 = SV_登录帐号.get()
    登录密码 = SV_登录密码.get()
    登录库名 = SV_登录库名.get()
    字符集 = SV_字符集.get()
    if 字符集.strip() == '':
        字符集 = 'utf8'
    R = DEV_MSSQL_OPEN(登录地址, 登录帐号, 登录密码, 服务端口, 登录库名, 字符集)
    if R[0] == 0:
        DB_INFO['数据库连接对象'] = R[1]      # 同步全局字典:保存数据库连接对象
        SV_数据库连接对象.set(str(R[1]))
        DEF_DB_查库_填入数据库树()
        SV_数据库树_选中库名.set(登录库名)
        TEXT_数据库操作日志.insert(0.0, 'MSSQL 连接成功\n', 'tag_i')
    else:
        ERROR = f'MSSQL 连接失败 {R[1]}'
        TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def 连接Oracle数据库():
    登录地址 = SV_登录地址.get()
    服务端口 = SV_服务端口.get()
    if 服务端口 == '':
        服务端口 = '1521'
    登录帐号 = SV_登录帐号.get()
    登录密码 = SV_登录密码.get()
    登录库名 = SV_登录库名.get()
    R = DEV_ORACLE_OPEN(登录帐号, 登录密码, 登录地址, 服务端口, 登录库名)
    if R[0] == 0:
        DB_INFO['数据库连接对象'] = R[1]      # 同步全局字典:保存数据库连接对象
        SV_数据库连接对象.set(str(R[1]))
        DEF_DB_查库_填入数据库树()
        SV_数据库树_选中库名.set(登录库名)
        TEXT_数据库操作日志.insert(0.0, 'Oracle 连接成功\n', 'tag_i')
    else:
        ERROR = f'Oracle 连接失败 {R[1]}'
        TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def 连接PostgreSQL数据库():
    登录地址 = SV_登录地址.get()
    服务端口 = SV_服务端口.get()
    if 服务端口 == '':
        服务端口 = '5432'
    登录帐号 = SV_登录帐号.get()
    登录密码 = SV_登录密码.get()
    登录库名 = SV_登录库名.get()
    R = DEV_PostgreSQL_OPEN(登录帐号, 登录密码, 登录地址, 服务端口, 登录库名)
    if R[0] == 0:
        DB_INFO['数据库连接对象'] = R[1]      # 同步全局字典:保存数据库连接对象
        SV_数据库连接对象.set(str(R[1]))
        DEF_DB_查库_填入数据库树()
        SV_数据库树_选中库名.set(登录库名)
        TEXT_数据库操作日志.insert(0.0, 'PostgreSQL 连接成功\n', 'tag_i')
    else:
        ERROR = f'PostgreSQL 连接失败 {R[1]}'
        TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def 打开或新建SQLite3数据库():
    DB_File = SV_SQLite3_数据库路径.get()     # 方便手动输入数据库名
    if DB_File.strip() == '':                 # 截掉头尾的空格后为空
        选择SQLite3数据库文件()
        DB_File = SV_SQLite3_数据库路径.get()
    if DB_File.strip() != '':                 # 截掉头尾的空格后不为空
        R = DEV_SQLite3_OPEN(DB_File)
        if R[0] == 0:
            DB_INFO['数据库连接对象'] = R[1]      # 同步全局字典:保存数据库连接对象
            SV_数据库连接对象.set(str(R[1]))      # DEBUG 查看
            DEF_DB_查库_填入数据库树()
        else:
            ERROR = f'打开数据库文件"{DB_File}"失败,错误信息{R[1]}'
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def DEF_按钮_关闭数据库():
    数据库连接对象 = DB_INFO['数据库连接对象']
    if 数据库连接对象 == '':
        ERROR = f"{SV_数据库类型.get()}数据库:未打开或已关闭"
        TEXT_数据库操作日志.insert(0.0, ERROR+'\n', 'tag_e')
    else:
        数据库连接对象.close()
        DB_INFO['数据库连接对象'] = ''
        SV_数据库连接对象.set('')
        TEXT_数据库操作日志.insert(0.0, f'{SV_数据库类型.get()}数据库:关闭\n', 'tag_i')
    FRAME_CLEAR(LF_显示编辑框)         # 清空框内控件
    ## 清理数据库树框
    items = Treeview_数据库结构.get_children()
    [Treeview_数据库结构.delete(item) for item in items]
    Log.debug(f"Treeview_数据库结构.delete(item) items={items}")
    
    ## 清空全局变量
    字典_查询字段_坐标_对象.clear()
    字典_查询字段_坐标_初值.clear()
    字典_查询结果_坐标_对象.clear()
    字典_查询结果_坐标_初值.clear()
    字典_添加记录_坐标_对象.clear()
    字典_创建表_字段信息.clear()
    字典_新加字段信息.clear()
    字典_对象存储.clear()
def 使用SQlite3模式():
    DEF_切换模式前置操作()
    Button_使用SQLite.config(bg='#FFD700')    # 按钮换个突出的颜色
    SV_数据库类型.set('SQLite3')
    Label(LF_数据库信息框, text='[SQLite3 路径]').grid(row=0,column=0,sticky='W')
    Entry(LF_数据库信息框, textvariable=SV_SQLite3_数据库路径, width=81).grid(row=0,column=1,sticky='W')
    按钮_打开数据库 = Button(LF_数据库信息框, text='打开/新建数据库', command=打开或新建SQLite3数据库)
    按钮_打开数据库.grid(row=1,column=0,sticky='NW')
    Button(LF_数据库信息框, text='关闭数据库', command=DEF_按钮_关闭数据库).grid(row=1,column=1,sticky='NW')
def 使用MySQL模式():
    DEF_切换模式前置操作()
    Button_使用MySQL.config(bg='#FFD700')    # 按钮换个突出的颜色
    SV_数据库类型.set('MySQL')
    SV_服务端口.set('3306')       # 默认端口号
    ## 登录信息框
    数据库登录框 = Frame(LF_数据库信息框)
    Label(数据库登录框, text='[帐号]').grid(                    row=0,column=0,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录帐号,width=10).grid(row=0,column=1,sticky='W')
    Label(数据库登录框, text='[密码]').grid(                    row=0,column=2,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录密码,width=10,show='*').grid(row=0,column=3,sticky='W')
    Label(数据库登录框, text='[地址]').grid(                    row=0,column=4,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录地址,width=15).grid(row=0,column=5,sticky='W')
    Label(数据库登录框, text='[端口]').grid(                    row=0,column=6,sticky='W')
    Entry(数据库登录框, textvariable=SV_服务端口,width=5).grid( row=0,column=7,sticky='W')
    Label(数据库登录框, text='[库名]').grid(                    row=0,column=8,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录库名,width=10).grid(row=0,column=9,sticky='W')
    Label(数据库登录框, text='[字符集]').grid(                  row=0,column=10,sticky='W')
    Entry(数据库登录框, textvariable=SV_字符集, width=8).grid(  row=0,column=11,sticky='W')
    数据库登录框.grid(row=0,column=0,sticky='NW')
    登录按钮框_MySQL = Frame(LF_数据库信息框)
    登录按钮框_MySQL.grid(row=2,column=0,sticky='NW')
    Button(登录按钮框_MySQL, text='连接数据库', command=连接MySQL数据库).grid(    row=0,column=0,sticky='NW')
    Button(登录按钮框_MySQL, text='断开数据库', command=DEF_按钮_关闭数据库).grid(row=0,column=1,sticky='NW')
    Button(登录按钮框_MySQL, text='载入连接配置文件', command=DEF_按钮_载入连接配置).grid(row=0,column=2,sticky='NW')
    Label(登录按钮框_MySQL, text='[配置文件]').grid(row=0,column=3,sticky='W')
    Entry(登录按钮框_MySQL, textvariable=SV_MySQL_CONF_PATH, width=51).grid(row=0,column=4,sticky='W')
def 使用Oracle模式():
    DEF_切换模式前置操作()
    Button_使用Oracle.config(bg='#FFD700')    # 按钮换个突出的颜色
    SV_数据库类型.set('Oracle')
    SV_服务端口.set('1521')       # 默认端口号
    ## 登录信息框
    数据库登录框 = Frame(LF_数据库信息框)
    Label(数据库登录框, text='[帐号]').grid(                    row=0,column=0,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录帐号,width=10).grid(row=0,column=1,sticky='W')
    Label(数据库登录框, text='[密码]').grid(                    row=0,column=2,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录密码,width=10,show='*').grid(row=0,column=3,sticky='W')
    Label(数据库登录框, text='[地址]').grid(                    row=0,column=4,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录地址,width=15).grid(row=0,column=5,sticky='W')
    Label(数据库登录框, text='[端口]').grid(                    row=0,column=6,sticky='W')
    Entry(数据库登录框, textvariable=SV_服务端口,width=5).grid( row=0,column=7,sticky='W')
    Label(数据库登录框, text='[实例]').grid(                    row=0,column=8,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录库名,width=10).grid(row=0,column=9,sticky='W')
    #Label(数据库登录框, text='[字符集]').grid(                  row=0,column=10,sticky='W')
    #Entry(数据库登录框, textvariable=SV_字符集, width=8).grid(  row=0,column=11,sticky='W')
    数据库登录框.grid(row=0,column=0,sticky='NW')
    登录按钮框_Oracle = Frame(LF_数据库信息框)
    登录按钮框_Oracle.grid(row=2,column=0,sticky='NW')
    Button(登录按钮框_Oracle, text='连接数据库', command=连接Oracle数据库).grid(   row=0,column=0,sticky='NW')
    Button(登录按钮框_Oracle, text='断开数据库', command=DEF_按钮_关闭数据库).grid(row=0,column=1,sticky='NW')
    Button(登录按钮框_Oracle, text='载入连接配置文件', command=DEF_按钮_载入连接配置).grid(row=0,column=2,sticky='NW')
    Label(登录按钮框_Oracle, text='[配置文件]').grid(row=0,column=3,sticky='W')
    Entry(登录按钮框_Oracle, textvariable=SV_Oracle_CONF_PATH, width=51).grid(row=0,column=4,sticky='W')
def 使用MSSQL模式():
    DEF_切换模式前置操作()
    Button_使用MSSQL.config(bg='#FFD700')    # 按钮换个突出的颜色
    SV_数据库类型.set('MSSQL')
    SV_服务端口.set('1433')       # 默认端口号
    ## 登录信息框
    数据库登录框 = Frame(LF_数据库信息框)
    Label(数据库登录框, text='[帐号]').grid(                    row=0,column=0,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录帐号,width=10).grid(row=0,column=1,sticky='W')
    Label(数据库登录框, text='[密码]').grid(                    row=0,column=2,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录密码,width=10,show='*').grid(row=0,column=3,sticky='W')
    Label(数据库登录框, text='[地址]').grid(                    row=0,column=4,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录地址,width=15).grid(row=0,column=5,sticky='W')
    Label(数据库登录框, text='[端口]').grid(                    row=0,column=6,sticky='W')
    Entry(数据库登录框, textvariable=SV_服务端口,width=5).grid( row=0,column=7,sticky='W')
    Label(数据库登录框, text='[库名]').grid(                    row=0,column=8,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录库名,width=10).grid(row=0,column=9,sticky='W')
    Label(数据库登录框, text='[字符集]').grid(                  row=0,column=10,sticky='W')
    Entry(数据库登录框, textvariable=SV_字符集, width=8).grid(  row=0,column=11,sticky='W')
    数据库登录框.grid(row=0,column=0,sticky='NW')
    登录按钮框_MSSQL = Frame(LF_数据库信息框)
    登录按钮框_MSSQL.grid(row=2,column=0,sticky='NW')
    Button(登录按钮框_MSSQL, text='连接数据库', command=连接MSSQL数据库).grid(   row=0,column=0,sticky='NW')
    Button(登录按钮框_MSSQL, text='断开数据库', command=DEF_按钮_关闭数据库).grid(row=0,column=1,sticky='NW')
    Button(登录按钮框_MSSQL, text='载入连接配置文件', command=DEF_按钮_载入连接配置).grid(row=0,column=2,sticky='NW')
    Label(登录按钮框_MSSQL, text='[配置文件]').grid(row=0,column=3,sticky='W')
    Entry(登录按钮框_MSSQL, textvariable=SV_MSSQL_CONF_PATH, width=51).grid(row=0,column=4,sticky='W')
def 使用PostgreSQL模式():
    DEF_切换模式前置操作()
    Button_使用PostgreSQL.config(bg='#FFD700')    # 按钮换个突出的颜色
    SV_数据库类型.set('PostgreSQL')
    SV_服务端口.set('5432')       # 默认端口号
    ## 登录信息框
    数据库登录框 = Frame(LF_数据库信息框)
    Label(数据库登录框, text='[帐号]').grid(                    row=0,column=0,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录帐号,width=10).grid(row=0,column=1,sticky='W')
    Label(数据库登录框, text='[密码]').grid(                    row=0,column=2,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录密码,width=10,show='*').grid(row=0,column=3,sticky='W')
    Label(数据库登录框, text='[地址]').grid(                    row=0,column=4,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录地址,width=15).grid(row=0,column=5,sticky='W')
    Label(数据库登录框, text='[端口]').grid(                    row=0,column=6,sticky='W')
    Entry(数据库登录框, textvariable=SV_服务端口,width=5).grid( row=0,column=7,sticky='W')
    Label(数据库登录框, text='[库名]').grid(                    row=0,column=8,sticky='W')
    Entry(数据库登录框, textvariable=SV_登录库名,width=10).grid(row=0,column=9,sticky='W')
    数据库登录框.grid(row=0,column=0,sticky='NW')
    登录按钮框_PostgreSQL = Frame(LF_数据库信息框)
    登录按钮框_PostgreSQL.grid(row=2,column=0,sticky='NW')
    Button(登录按钮框_PostgreSQL, text='连接数据库', command=连接PostgreSQL数据库).grid(   row=0,column=0,sticky='NW')
    Button(登录按钮框_PostgreSQL, text='断开数据库', command=DEF_按钮_关闭数据库).grid(row=0,column=1,sticky='NW')
    Button(登录按钮框_PostgreSQL, text='载入连接配置文件', command=DEF_按钮_载入连接配置).grid(row=0,column=2,sticky='NW')
    Label(登录按钮框_PostgreSQL, text='[配置文件]').grid(row=0,column=3,sticky='W')
    Entry(登录按钮框_PostgreSQL, textvariable=SV_PostgreSQL_CONF_PATH, width=51).grid(row=0,column=4,sticky='W')

## 其他功能:SQL示例,根据字典自动生成多级菜单
SQL语句示例字典 = {
    "库操作":
    {
        "创建库":"CREATE DATABASE 库名;",
    },
    "表操作":
    {
        "修改表名":"ALTER TABLE 旧表名 RENAME 新表名;",
        "增加字段":"ALTER TABLE 表名 ADD 新列名 VARCHAR(10)",
        "增加字段(记录修改时间)":"ALTER TABLE 表名 ADD column 新列名 timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;",
        "增加字段(记录创建时间)":"ALTER TABLE 表名 ADD column 新列名 timestamp DEFAULT CURRENT_TIMESTAMP;",
        "删除字段":"ALTER TABLE 表名 DROP 列名;",
        "修改字段属性":"ALTER TABLE 表名 MODIFY 字段名 VARCHAR(200) NOT NULL DEFAULT '0000';",
        "修改字段名":"ALTER TABLE 表名 CHANGE 原字段名 新字段名 字段类型 约束条件",
        "修改字段位置":"ALTER TABLE user10 MODIFY card CHAR(10) AFTER test; -- 将card移到test后面",
        "清空表内容":"DELETE FROM 表名",
        "清空表内容及重置Identity(标识列、自增字段)":"truncate table 表名",
        "清空表内容及表结构":"DROP TABLE 表名"
    },
    "INSERT":
    {
        "插入一个字段":"INSERT INTO 表名 (列名) VALUES ('值');",
        "插入多个字段":"INSERT INTO 表名 (列名1, 列名2) VALUES ('值1', '值2');"
    },
    "DELETE":
    {
        "删除":"DELETE FROM 表名 WHERE 列名 = '值';"
    },
    "UPDATE":
    {
        "更新一个字段":"UPDATE 表名 SET 列名 = '新值' WHERE 某列名 = '原值';",
        "更新多个字段":"UPDATE 表名 SET 列名1 = '新值', 列名2 = '新值' WHERE 某列名 = '原值';"
    },
    "SELECT":
    {
        "全部数据":"SELECT * FROM 表名;",
        "部分数据":"SELECT 字段1,字段2 FROM 表名 WHERE 字段=;",
        "结果去重":"SELECT DISTINCT 列名 FROM 表名;",
        "判断空值 IS NULL":"SELECT * FROM 表名 WHERE 列名 IS NULL;",
        "判断非空值 IS NOT NULL":"SELECT * FROM 表名 WHERE 列名 IS NOT NULL;",
        "模糊查询 拼接":"拼接",
        "模糊查询":"SELECT * FROM 表名 WHERE 字段 LIKE '%查找内容%';",
        "模糊查询 排除":"SELECT * FROM 表名 WHERE 列名 NOT LIKE '%查找内容%';",
        "取反":"SELECT * FROM 表名 WHERE 列名 LIKE '[^12]0';",
        "区间":"SELECT * FROM 表名 WHERE 列名 BETWEEN 1 AND 30; -- 查询 列名范围在1到30间的记录",
        "判断 IN":"SELECT * FROM 表名 WHERE 字段 IN ('1','2'); -- 查询字段内容是 1 或 2 的记录",
        "判断 OR":"SELECT * FROM 表名 WHERE 字段 = '1' OR 字段 = '2'; -- 查询字段内容是 1 或 2 的记录",
        "判断 AND":"SELECT * FROM 表名 WHERE 字段 != '1' AND 字段 != '2'; -- 查询字段内容不是 1 或 2 的记录",
        "排序 升序":"SELECT * FROM 表名 ORDER BY 列名 ASC; -- 升序(默认)",
        "排序 降序":"SELECT * FROM 表名 ORDER BY 列名 DESC; -- 降序",
        "筛选出重复记录":"SELECT * FROM 表名 where 列名 in (select 列名 from 表名 group by 列名 having count(列名) > 1);",
        "聚集函数 计数 COUNT(*)":"SELECT COUNT(*) FROM 表名; -- 统计全记录数 ",
        "聚集函数 总和 SUM()":"",
        "聚集函数 平均 AVG()":"",
        "聚集函数 最大 MAX()":"",
        "聚集函数 最小 MIN()":"",
        "分组 GROUP BY 子句":"SELECT name,COUNT(ID) FROM 表名 GROUP BY name; -- 同一个名字有多少个ID号(以名字分组,相同名字为一组)",
        "GROUP BY 和 HAVING":"SELECT name,COUNT(ID) FROM 表名 GROUP BY name HAVING COUNT(ID)>3; -- 同一个名字有多少个ID号(以名字分组,相同名字为一组)再在结果中筛选出数量大于3的",
        "模糊查询":"SELECT * FROM 表名 WHERE CONCAT(字段1,字段2) LIKE '%查找内容%';"
    }}
一级菜单 = Menu(tearoff=False)  # 创建一级菜单对象
def DEF_点击二级菜单(K1,K2):
    二级菜单选中值 = SQL语句示例字典[K1][K2]
    文本框_命令行.delete(0.0, END)
    文本框_命令行.insert(0.0, 二级菜单选中值)
    文本框_命令行.focus_set()
for K1 in SQL语句示例字典:
    二级菜单 = Menu(tearoff=False)      # 创建二级菜单对象
    for K2 in SQL语句示例字典[K1]:
        二级菜单.add_command(label=K2, command=lambda STR1=K1,STR2=K2:DEF_点击二级菜单(STR1,STR2))  # 二级菜单添加'一级KEY'及'二级KEY'
    一级菜单.add_cascade(label=K1, menu=二级菜单)                                                   # 一级菜单添加'菜单项'并关联'二级菜单'
def DEF_选择SQL示例语句():
    一级菜单_X = Button_示例SQL语句.winfo_rootx() + Button_示例SQL语句.winfo_width()
    一级菜单_Y = Button_示例SQL语句.winfo_rooty() - Button_示例SQL语句.winfo_height()
    一级菜单.post(一级菜单_X, 一级菜单_Y)

#################################################
## TKinter 主窗口布局 - TOP框 - 顶级功能菜单框 ##
Label_安全模式 = Label(顶级功能菜单框, text='           [安全模式]')
Label_安全模式.grid(row=0,column=10,sticky='W')
Combobox_安全模式状态 = ttk.Combobox(顶级功能菜单框, width=4)
Combobox_安全模式状态['value'] = ('关闭', '开启')
Combobox_安全模式状态.current(0)                              # 默认值中的内容为索引,从0开始
Label_安全模式['fg'] = 'OrangeRed'
Combobox_安全模式状态.grid(row=0, column=11, sticky='W')
def DEF_设置安全模式状态(event):
    SV_安全模式状态.set(Combobox_安全模式状态.get())
    if Combobox_安全模式状态.get() == '关闭':
        Label_安全模式['fg'] = 'OrangeRed'
    else:
        Label_安全模式['fg'] = 'Green'
Combobox_安全模式状态.bind('<<ComboboxSelected>>', DEF_设置安全模式状态)
SV_安全模式状态.set(Combobox_安全模式状态.get())
Button_使用SQLite = Button(顶级功能菜单框, text='SQLite3', command=使用SQlite3模式, width=10)
Button_使用MySQL = Button(顶级功能菜单框, text='MySQL', command=使用MySQL模式, width=10)
Button_使用Oracle = Button(顶级功能菜单框, text='Oracle', command=使用Oracle模式, width=10)
Button_使用MSSQL = Button(顶级功能菜单框, text='SQLServer', command=使用MSSQL模式, width=10)
Button_使用PostgreSQL = Button(顶级功能菜单框, text='PostgreSQL', command=使用PostgreSQL模式, width=10)
Button_示例SQL语句 = Button(顶级功能菜单框, text='[ 示例SQL语句 ]', command=DEF_选择SQL示例语句)
Button_使用SQLite.grid(row=0,column=0,sticky='NW')
Button_使用MySQL.grid(row=0,column=1,sticky='NW')
Button_使用Oracle.grid(row=0,column=2,sticky='NW')
Button_使用MSSQL.grid(row=0,column=3,sticky='NW')
Button_使用PostgreSQL.grid(row=0,column=4,sticky='NW')
Button_示例SQL语句.grid(row=0,column=9,sticky='E')
使用SQlite3模式()   ## 默认开启SQLite3模式


##########################################################
## TKinter 主窗口布局 - TOP框 - 控表按钮框 - 分页按钮框 ##
def DEF_按钮_显编框起始页():
    执行上条查询语句_刷新显示('从头开始')
def DEF_按钮_显编框上一页():
    Log.debug("DEF_按钮_显编框上一页")
    分页限制行数 = IV_设置分页行数.get()
    已显示记录数 = IV_已显示记录数.get()
    当前显示行数 = IV_上次分页行数.get()
    上一页可显示最大行数 = 已显示记录数-当前显示行数
    if 上一页可显示最大行数 > 0:                  # 上一页有内容
        if 分页限制行数 >= 上一页可显示最大行数:  # 上一页内容不足一个分页,全部显示到一个分页
            Log.debug(f"从头显示 (分页限制行数){分页限制行数}>={上一页可显示最大行数}(上一页可显示最大行数)")
            执行上条查询语句_刷新显示('从头开始')
            tkinter.messagebox.showinfo(title='INFO', message='已经到最前面')
        else:
            定位显示开始位置行号 = 已显示记录数-当前显示行数-分页限制行数
            Log.debug(f"往前显示{分页限制行数}(分页限制行数)行数据(定位显示开始位置行号={定位显示开始位置行号}) (已显示记录数){已显示记录数}>{分页限制行数}(分页限制行数)")
            最后查询语句 = DB_INFO['最后查询语句']
            if 最后查询语句 != '':
                DEF_SQL_查询和显示(最后查询语句, 定位显示开始位置行号)
            else:
                ERROR = "找不到上次执行的查询语句 DB_INFO['最后查询语句']=''"
                Log.error(ERROR)
                tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        ERROR = f'(已显示记录数){已显示记录数}-{当前显示行数}(当前显示行数)={上一页可显示最大行数}(上一页可显示最大行数) 判断<=0 无上一页内容'
        Log.error(ERROR)
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
def DEF_按钮_显编框下一页():
    Log.debug("DEF_按钮_显编框下一页")
    DEF_SQL_查询和显示_下一页()
def DEF_键盘事件_提取数字部分(event):
    输入值 = SV_设置分页行数.get()
    提取数字部分 = re.sub('[^0-9]', '', 输入值)  # 非数字的部分都删除
    Log.debug(f"Enter_分页显示行数 获取输入值={输入值} 提取数字部分={提取数字部分}")
    if 提取数字部分 == '':
        IV_设置分页行数.set(0)
        SV_设置分页行数.set(0)
    else:
        IV_设置分页行数.set(int(提取数字部分))
        SV_设置分页行数.set(int(提取数字部分))
分页按钮框 = Frame(控表按钮框)
分页按钮框.grid(row=1,column=0,sticky='NW')
按钮_显编框起始页 = Button(分页按钮框, text='返回起始页/刷新', command=DEF_按钮_显编框起始页)
按钮_显编框上一页 = Button(分页按钮框, text='上一页', command=DEF_按钮_显编框上一页)
按钮_显编框下一页 = Button(分页按钮框, text='下一页', command=DEF_按钮_显编框下一页)
按钮_显编框起始页.grid(row=0,column=0, sticky='NW') # 显示起始页按钮
按钮_显编框上一页.grid(row=0,column=1, sticky='NW') # 显示上一页按钮
按钮_显编框下一页.grid(row=0,column=2, sticky='NW') # 显示下一页按钮
Label(分页按钮框, text='[分页显示行数]').grid(row=0,column=3,sticky='W')
Enter_分页显示行数 = Entry(分页按钮框, textvariable=SV_设置分页行数, width=4)
Enter_分页显示行数.grid(row=0, column=4, sticky='W')
Enter_分页显示行数.bind('<KeyRelease>', DEF_键盘事件_提取数字部分)
Label(分页按钮框, text='          [单元格限宽]').grid(row=0,column=5,sticky='W')
Entry(分页按钮框, textvariable=IV_单元格限宽, width=4).grid(row=0,column=6,sticky='W')
Label(分页按钮框, text='          [自定义空值表示]').grid(row=0,column=7,sticky='W')
Entry(分页按钮框, textvariable=SV_自定义空值字符串表示, width=8).grid(row=0,column=8,sticky='W')
按钮_显编框下一页['state'] = 'disabled' ## 不能用时禁止
#按钮_显编框下一页['state'] = 'normal'  ## 能用时再启用


#########################################
## TKinter 主窗口布局 - TOP框 - 命令框 ##
def DEF_按钮_执行查询语句():
    SQL_CMD = 文本框_命令行.get(1.0, END).rstrip('\n')    # 获取编写的SQL语句,去掉后面的回车符号
    if SQL_CMD.strip() != '':
        DEF_SQL_查询和显示(SQL_CMD)                       # 调用查询语句专用函数
    else:
        ERROR = '没有输入SQL语句'
        tkinter.messagebox.showerror(title='错误', message=ERROR)
def DEF_按钮_反悔三件套_执行非查询语句_不提交():
    SQL_CMD = 文本框_命令行.get(1.0, END).rstrip('\n')    # 获取编写的SQL语句,去掉后面的回车符号
    if SQL_CMD.strip() != '':
        ## 判断是单条SQL语句还是多条
        L = SQL_CMD.split(';')
        if len(L) == 1 or (len(L) == 2 and L[1].strip() == ''):
            R = DEF_SQL_执行_不自动提交(SQL_CMD)
        else:
            R = DEF_执行脚本或批处理_不自动提交修改(SQL_CMD)
        
        if R[0] == 0:
            ## 操作成功后更新一下显示/编辑框
            执行上条查询语句_刷新显示('从头开始')
            tkinter.messagebox.showinfo(title='成功', message=R[1]+'\n【注意】\n确认修改须点 [提交] 按钮\n回撤操作须点 [回退] 按钮')
        else:
            tkinter.messagebox.showerror(title='失败', message=R[1]+'\n【注意】\n确认修改须点 [提交] 按钮\n回撤操作须点 [回退] 按钮')
    else:
        ERROR = '空SQL'
        tkinter.messagebox.showerror(title='错误', message=ERROR)
def DEF_按钮_反悔三件套_提交():
    数据库连接对象 = DB_INFO['数据库连接对象']
    if 数据库连接对象 != '':
        数据库连接对象.commit()        # 提交,确认修改
        TEXT_数据库操作日志.insert(0.0, '提交\n', 'tag_w')
    else:
        tkinter.messagebox.showerror(title='错误', message='没有可提交对象')
def DEF_按钮_反悔三件套_回退():
    数据库连接对象 = DB_INFO['数据库连接对象']
    if 数据库连接对象 != '':
        数据库连接对象.rollback()      # 回退,撤销修改
        TEXT_数据库操作日志.insert(0.0, '回退\n', 'tag_w')
    else:
        tkinter.messagebox.showerror(title='错误', message='没有可回退对象')
def DEF_按钮_清屏命令框():
    文本框_命令行.delete(0.0, END)
    文本框_命令行.focus_set()
def DEF_选择SQL脚本文本():
    本地SQL脚本文本 = filedialog.askopenfilename()
    DB_SQL_SCRIPT_FILE.set(本地SQL脚本文本)              # 实时更新显示
def DEF_按钮_执行SQL脚本文件():
    脚本文件 = DB_SQL_SCRIPT_FILE.get().strip()
    if 脚本文件.strip() == '':
        DEF_选择SQL脚本文本()
        脚本文件 = DB_SQL_SCRIPT_FILE.get().strip()
    if 脚本文件.strip() != '':
        try:
            f = open(脚本文件, 'r', encoding='utf-8')
            脚本文件内容 = f.read()
            f.close()
        except Exception as e:
            ERROR = str(e)
            tkinter.messagebox.showerror(title='错误', message=ERROR)
        else:
            if 脚本文件内容 != '':
                R = DEF_执行脚本或批处理_不自动提交修改(脚本文件内容)
                if R[0] == 0:
                    执行上条查询语句_刷新显示('从头开始')
                    tkinter.messagebox.showinfo(title='成功', message=R[1])
                else:
                    tkinter.messagebox.showerror(title='失败', message=R[1])
            else:
                ERROR = 'SQL脚本文件无内容'
                tkinter.messagebox.showerror(title='错误', message=ERROR)

命令行_按钮框 = Frame(命令框)
命令行_按钮框.grid(row=0, column=0, sticky='W')
Button(命令行_按钮框, text='执行查询语句', command=DEF_按钮_执行查询语句).grid(row=0, column=0)
Button(命令行_按钮框, text='清屏', command=DEF_按钮_清屏命令框, width=25, bg='#FFFFFF').grid(row=0, column=1)
Button(命令行_按钮框, text='执行修改语句/脚本/批处理', command=DEF_按钮_反悔三件套_执行非查询语句_不提交, width=31, bg='#40E0D0').grid(row=0, column=2)
Button(命令行_按钮框, text='提交', command=DEF_按钮_反悔三件套_提交, width=10, bg='#40E0D0').grid(row=0, column=4)
Button(命令行_按钮框, text='回退', command=DEF_按钮_反悔三件套_回退, width=10, bg='#40E0D0').grid(row=0, column=5)

文本框_命令行 = Text(命令框, height=3, wrap='none')
文本框_命令行.grid(row=1,column=0,sticky='NESW')
Scrollbar_命令框_竖 = Scrollbar(命令框)
Scrollbar_命令框_竖['command'] = 文本框_命令行.yview
Scrollbar_命令框_横 = Scrollbar(命令框)
Scrollbar_命令框_横['command'] = 文本框_命令行.xview
Scrollbar_命令框_横['orient'] = HORIZONTAL
Scrollbar_命令框_竖.grid(row=1, column=1, sticky='NSEW')
Scrollbar_命令框_横.grid(row=2, column=0, sticky='NSEW')
文本框_命令行.config(xscrollcommand=Scrollbar_命令框_横.set, yscrollcommand=Scrollbar_命令框_竖.set)  # 自动设置滚动条滑动幅度
本地SQL脚本文件操作框 = Frame(命令框)
本地SQL脚本文件操作框.grid(row=3, column=0, sticky='W')
Button(本地SQL脚本文件操作框, text='执行脚本文件', command=DEF_按钮_执行SQL脚本文件).grid(row=0, column=0, sticky='W')
Label(本地SQL脚本文件操作框, text='[脚本文件路径]').grid(row=0, column=1, sticky='W')
Entry(本地SQL脚本文件操作框, textvariable=DB_SQL_SCRIPT_FILE, width=71).grid(row=0, column=2)

## END ##
top.mainloop()  ## 进入消息循环
## END ##

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: sqlite3 模块是 Python 的标准库之一,用于在 Python 程序中使用 SQLite 数据库SQLite 是一种轻量级的关系数据库管理系统,可以被嵌入到应用程序中。使用 sqlite3 模块,您可以在 Python 程序中创建数据库连接,执行 SQL 语句,并获取结果。 下面是使用 sqlite3 模块的简单示例: ``` import sqlite3 # 连接到数据库 conn = sqlite3.connect('example.db') # 创建一个游标 cursor = conn.cursor() # 执行一条 SQL 语句 cursor.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)') # 插入一条记录 cursor.execute('INSERT INTO users (name, age) VALUES (?, ?)', ('Alice', 20)) # 提交事务 conn.commit() # 查询数据 cursor.execute('SELECT * FROM users') # 获取查询结果 result = cursor.fetchall() # 打印查询结果 print(result) # 关闭连接 conn.close() ``` 在这个示例中,我们使用 sqlite3.connect() 函数连接到一个数据库文件,然后使用 cursor.execute() 执行 SQL 语句。我们还使用了占位符(问号)来安全地执行有参数的 SQL 语句。最后,我们使用 cursor.fetchall() 获取查询结果并打印出来。 ### 回答2: Pythonsqlite3模块是一个用于操作SQLite数据库的内置模块。它提供了一系列函数和类,可以通过编程的方式进行数据库的连接、查询、更新和管理。 首先,我们需要使用sqlite3模块中的connect()函数来建立与数据库的连接。连接成功后,我们可以获得一个数据库连接对象,可以使用这个对象执行各种数据库操作sqlite3模块中的execute()函数可以执行SQL语句,例如创建表、插入数据、更新数据、删除数据等。我们可以使用该函数来执行需要的SQL语句,并通过commit()函数进行事务的提交。 查询数据可以使用execute()函数执行SELECT语句,并通过fetchone()函数获取一条查询结果,或者使用fetchall()函数获取所有查询结果。 sqlite3模块还提供了许多其他的函数和类,用于事务控制、错误处理和数据库管理。例如,我们可以使用rollback()函数进行事务的回滚,使用rowcount属性获取执行SQL语句后的受影响行数,还可以使用Cursor对象来进行游标操作和预编译语句执行等。 总之,Pythonsqlite3模块提供了一种方便、灵活和高效的方式来操作SQLite数据库。无论是小型项目还是大型项目,都可以使用sqlite3模块来进行数据库操作和管理。它具有简单易用的特点,适合初学者学习和使用。 ### 回答3: Pythonsqlite3 模块是用来操作 SQLite 数据库的一个内置模块。SQLite 是一种轻型的嵌入式数据库,不需要一个独立的服务器进程,而是直接读取和写入普通的磁盘文件。sqlite3 模块提供了一种简单、直观的方式来创建、连接、查询、操作和管理 SQLite 数据库使用 sqlite3 模块,我们可以通过以下步骤来操作 SQLite 数据库: 1. 导入 sqlite3 模块:首先需要导入 sqlite3 模块,这样我们才能在 Python 程序中使用 SQLite 数据库的相关功能函数和类。 2. 连接数据库:通过 sqlite3 模块提供的 connect() 函数,我们可以连接到一个指定的 SQLite 数据库文件,如果文件不存在则会自动创建。 3. 创建表:使用 SQL 语句,可以在连接的数据库中创建表格,定义表格的字段名和类型等。 4. 执行 SQL 语句:在连接的数据库上,可以使用 execute() 函数执行任意的 SQL 语句,比如插入数据、查询数据等。 5. 提交事务:对于需要修改数据库操作,需要在操作完成之后使用 commit() 函数提交事务,使得修改生效。 6. 关闭数据库连接:在不再使用数据库时,通过 close() 函数关闭数据库连接,释放资源。 sqlite3 模块简了与 SQLite 数据库的交互过程,提供了一系列的函数和类来实现数据库的访问和管理。这些函数和类包括 connect()、execute()、fetchone()、fetchall() 等,它们可以帮助我们完成数据库查询、增加、删除、修改等操作。 总之,Pythonsqlite3 模块为我们提供了一个方便、快速且易于理解的方式来处理 SQLite 数据库,使得我们可以方便地进行数据库操作,并且无需安装任何额外的数据库软件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值