#_*_ coding:utf8 _*_
## Python3-GUI-DB-MySQL
## V1.5 新增根据字段及数据的字符数自动调整显示单元格宽度

import math                     # 自动设置单元格宽度用,向上取整

import MySQLdb                  # MySQL 模块,使用 pip install mysqlclient 安装
import re
from tkinter import *
from tkinter import filedialog  # 选择文件用
from tkinter import ttk         # 下拉菜单控件在ttk中
import tkinter.messagebox       # 弹出提示对话框
import tkinter.simpledialog     # 弹出对话框,获取用户输入
import os                       # 导出文件要用到
import time                     # 导出文件要用到
import csv                      # CSV文件操作模块,用于导出数据
#from openpyxl import Workbook   # Excel文件操作模块,用于导出数据(第三方模块,在需要时加载)

import logging                                                             # 日志模块
Log = logging.getLogger('__name__')                                        # 获取实例
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')   # 指定logger输出格式
file_handler = logging.FileHandler('PY3_MySQL.log')                        # 日志文件路径
file_handler.setFormatter(formatter)                                       # 可以通过setFormatter指定输出格式
Log.addHandler(file_handler)                                               # 为logger添加的日志处理器
# 设置记录的日志级别
Log.setLevel(logging.DEBUG)
#Log.setLevel(logging.INFO)
#Log.setLevel(logging.WARNING)
#Log.setLevel(logging.ERROR)
#Log.setLevel(logging.CRITICAL)



##################
## 全局字典变量 ##
##################

## 存储数据库信息
DB_INFO = {'数据库文件':'', '数据库连接':'', '数据库游标':'', '字段名列表':[], 'LAST_SELECT':''}
字典_查询字段_坐标_对象 = {}  # KEY=控件坐标 || VAULE=控件对象 || {(控件行号,控件列号):控件对象} || { (0,0):obj }
字典_查询字段_坐标_初值 = {}  # KEY=控件坐标 || VAULE=初始值   || {(控件行号,控件列号):初始值}   || { (0,0):123 }
字典_查询结果_坐标_对象 = {}  # KEY=控件坐标 || VAULE=控件对象 || {(控件行号,控件列号):控件对象} || { (0,0):obj }
字典_查询结果_坐标_初值 = {}  # KEY=控件坐标 || VAULE=初始值   || {(控件行号,控件列号):初始值}   || { (0,0):123 }
字典_添加记录_坐标_对象 = {}  # KEY=控件坐标 || VAULE=控件对象 || {(控件行号,控件列号):控件对象} || { (0,0):obj }
字典_添加记录_坐标_初值 = {}  # KEY=控件坐标 || VAULE=初始值   || {(控件行号,控件列号):初始值}   || { (0,0):123 }
字典_创建表_字段信息 = {}
字典_新加字段信息 = {}
字典_对象存储 = {}            # {'文本编辑对象':''} 大文本编辑框用

字典_新数据表表名对象 = {}    # 修改数据表名用,存放新数据表名输入框对象



################
## MySQL 操作 ##
################

## 打开数据库:尝试连接数据库并查询数据库中的全部数据表表名
def DEV_MySQL_OPEN(登录地址, 登录帐号, 登录密码, 服务端口, 登录库名, 字符集='utf8'):
    try:
        数据库连接对象 = MySQLdb.connect(登录地址, 登录帐号, 登录密码, 登录库名, port=服务端口, charset=字符集)
    except Exception as e:
        E = '连接数据库失败 ' + str(e)
        return(1, E)                                        # 返回错误代码1和失败原因
    else:
        print("连接数据库成功")
        try:
            游标对象 = 数据库连接对象.cursor()              # 创建一个游标
        except Exception as e:
            ERROR = '创建游标失败' + str(e)
            return(1, ERROR)
        else:
            print("创建游标成功")
            try:
                游标对象.execute('SHOW TABLES;')
            except Exception as e:
                ERROR = f'数据库"{登录库名}"执行SQL语句"SHOW TABLES;"失败:{e}'
                return(1, ERROR)
            else:
                print("查询数据库中数据表成功")
                全部记录 = 游标对象.fetchall()              # 元组类型,空为()
                print("全部记录", 全部记录)
                游标对象.close()
                print("关闭游标")
                DB_INFO['数据库连接'] = 数据库连接对象      # 同步全局字典:保存数据库连接对象
                if 全部记录 == ():
                    STR_数据表列表内容.set('<空>')
                else:
                    数据表列表 = [i[0] for i in 全部记录]
                    STR_数据表列表内容.set(数据表列表)
                return(0,)



##########
## 函数 ##
##########

### MySQL 查找主键
### 查询数据表字段名信息
# 新增记录使用
def DEF_TABLE_COLUMN_NAME(TABLE_NAME):
    SQL_CMD = f'SELECT COLUMN_NAME FROM information_schema.columns WHERE table_name="{TABLE_NAME}"'
    R = DEF_SQL_查询和返回(SQL_CMD)
    if R[0] == 0:
        查询结果 = R[1]
        print("查询结果", 查询结果)             # 查询结果 (('ID',), ('USER',), ('PASS',), ('TIME',))
        字段名列表 = [i[0] for i in 查询结果]
        return(0, 字段名列表)
    else:
        return(1, R[1])

## 查询数据表的主键,返回主键名列表
# 修改数据、删除记录使用
def 主键名列表(数据表名):
    SQL_CMD = f'SELECT COLUMN_NAME, COLUMN_KEY FROM information_schema.columns WHERE table_name="{数据表名}"'
    R = DEF_SQL_查询和返回(SQL_CMD)
    if R[0] == 0:
        查询结果 = R[1]                 # ((),())
        L_PK_NAME = []                  # 主键字段名列表
        for i in 查询结果:
            if i[1] == 'PRI':           # 第2列为PRI表示是主键
                L_PK_NAME.append(i[0])  # 字段名名信息在第1列
        return(0, L_PK_NAME)
    else:
        ERROR = f'查询主键字段信息失败:{R[1]}'
        return(1, ERROR)

## 执行更新显示/编辑框(从头显示)
def UPDATE_SELECT():
    LAST_SELECT = DB_INFO['LAST_SELECT']
    if LAST_SELECT == '':
        WARNING = '上一步查询语句为空'
        print(WARNING)
    else:
        print("从头查询显示")
        DEF_SQL_查询和显示(LAST_SELECT)

## 执行更新显示/编辑框(从编辑处显示)
### 用于修改数据库后,再次显示在修改位置
def UPDATE_SELECT_LINIT():
    LAST_SELECT = DB_INFO['LAST_SELECT']
    if LAST_SELECT == '':
        WARNING = '上一步查询语句为空'
        print(WARNING)
    else:
        显编框修改处定位 = IV_已显示记录数.get() - IV_上次分页行数.get()
        if 显编框修改处定位 <= 0:
            print("从头查询显示")
            DEF_SQL_查询和显示(LAST_SELECT)
        else:
            print("从修改处查询显示")
            IV_已显示记录数.set(显编框修改处定位)
            DEF_SQL_查询和显示_定位到编辑处(LAST_SELECT, 显编框修改处定位)

## 执行SQL查询语句,返回执行状态和执行结果(数据列表)
def DEF_SQL_查询和返回(SQL_CMD):
    数据库连接对象 = DB_INFO['数据库连接']
    try:
        游标对象 = 数据库连接对象.cursor()     # 创建一个游标
    except Exception as e:
        ERROR = '创建游标失败' + str(e)
        return(1, ERROR)
    else:
        #print("创建游标")
        try:
            游标对象.execute(SQL_CMD)
        except Exception as e:
            ERROR = f'数据库 {DB_NAME.get()} 执行SQL语句 {SQL_CMD} 失败 {e}'
            return(1, ERROR)
        else:
            全部记录 = 游标对象.fetchall()
            游标对象.close()
            #print("关闭游标")
            DB_INFO['数据库游标'] = ''
            return(0, 全部记录)

## 执行SQL查询语句,返回执行状态和执行结果(数据列表,字段列表)
## 导出使用
def DEF_SQL_查询和返回_数据列表_字段列表(SQL_CMD):
    数据库连接对象 = DB_INFO['数据库连接']
    try:
        游标对象 = 数据库连接对象.cursor()
    except Exception as e:
        ERROR = '创建游标失败' + str(e)
        return(1, ERROR)
    else:
        #print("创建游标", 游标对象)
        try:
            游标对象.execute(SQL_CMD)
        except Exception as e:
            ERROR = f'数据库 {DB_NAME.get()} 执行SQL语句 {SQL_CMD} 失败 {str(e)}'
            游标对象.close()
            #print("关闭游标", 游标对象)
            return(1, ERROR)
        else:
            全部记录 = 游标对象.fetchall()                       # 获取全部查询数据记录
            游标对象_字段名列表 = 游标对象.description           # 获取查询结果的字段信息
            字段名列表 = [i[0] for i in 游标对象_字段名列表]     # 整理成字段名列表
            游标对象.close()
            #print("关闭游标", 游标对象)
            return(0, 全部记录, 字段名列表)

## 执行SQL查询语句,直接显示在界面,不返回
def DEF_SQL_查询和显示(SQL_CMD):
    数据库连接对象 = DB_INFO['数据库连接']
    try:
        游标对象 = 数据库连接对象.cursor()                       # 创建一个游标
    except Exception as e:
        ERROR = '创建游标失败' + str(e)
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        print("创建游标(预备缓存游标,用于读取显示下一页)")
        try:
            游标对象.execute(SQL_CMD)
        except Exception as e:
            ERROR = f'数据库 {DB_NAME.get()} 执行SQL语句 {SQL_CMD} 失败 {e}'
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
        else:
            DB_INFO['LAST_SELECT'] = SQL_CMD                     # 保存成功执行的SQL查询语句
            SV_最后查询语句.set(SQL_CMD)                         # 保存成功执行的SQL查询语句
            游标对象_字段名列表 = 游标对象.description           ## 获取当次查询的字段名信息
            字段名列表 = [i[0] for i in 游标对象_字段名列表]
            DB_INFO['字段名列表'] = 字段名列表                   # 保存字段名查询结果
            SV_查询字段列表.set(字段名列表)                      # 展示字段名查询结果
            IV_已显示记录数.set(0)                               ### 用于修改数据库后,再次显示在修改位置
            IV_上次分页行数.set(0)                               ### 用于修改数据库后,再次显示在修改位置
            查询结果总行数 = 游标对象.rowcount                   # 受影响行数:获取查询记录总行数
            IV_记录总行数.set(查询结果总行数)

            ## 分页控制
            if 查询结果总行数 == 0:
                print("查询结果为空(不用分页)")
                字段和数据的存储和展示(字段名列表, [])
                游标对象.close()                                 # 关闭游标对象
                DB_INFO['数据库游标'] = ''                       # 清空数据库游标对象
                按钮_显编框下一页['state'] = 'disabled'          # 禁止下一页按钮
                WARNING = '查询结果为空'
                tkinter.messagebox.showwarning(title='WARNING', message=WARNING)
            else:
                分页限制行数 = 分页行数.get()
                if 分页限制行数 <= 0:
                    WARNING = '为防止一次显示数据过多,请务必设置分页行数(分页显示行数>0)'
                    tkinter.messagebox.showwarning(title='WARNING', message=WARNING)
                else:
                    if 分页限制行数 >= 查询结果总行数:                                  # 可显示行数大于实际数据记录数量
                        print("【读取全部】分页限制行数 >= 查询结果总行数,不分页")
                        全部记录 = 游标对象.fetchall()                                  # 从SQL查询结果中取出全部的记录
                        字段和数据的存储和展示(字段名列表, 全部记录)
                        游标对象.close()                                                # 关闭游标对象
                        print("关闭游标")
                        DB_INFO['数据库游标'] = ''                                      # 清空数据库游标对象
                        按钮_显编框下一页['state'] = 'disabled'                         # 禁止下一页按钮
                        #实际读取记录行数 = len(全部记录)                               ### 用于修改数据库后,再次显示在修改位置
                        实际读取记录行数 = 查询结果总行数                               ### 用于修改数据库后,再次显示在修改位置
                        IV_已显示记录数.set(IV_已显示记录数.get() + 实际读取记录行数)   ### 用于修改数据库后,再次显示在修改位置
                        IV_上次分页行数.set(实际读取记录行数)                           ### 用于修改数据库后,再次显示在修改位置
                    else:
                        print("【读取部分】分页限制行数 <  查询结果总行数,分页")
                        部分记录 = 游标对象.fetchmany(分页限制行数)                     # 从SQL查询结果中取指定行数的记录,如果查询结果为空则返回空列表
                        字段和数据的存储和展示(字段名列表, 部分记录)                    # 在显编框展示结果,保存结果到全局变量,可以进行修改操作
                        ## 保存游标对象,以便继续读取
                        DB_INFO['数据库游标'] = 游标对象                                # 保存数据库游标对象
                        print("缓存游标")
                        按钮_显编框下一页['state'] = 'normal'                           # 恢复下一页按钮
                        #实际读取记录行数 = len(部分记录)
                        实际读取记录行数 = 分页限制行数
                        IV_已显示记录数.set(IV_已显示记录数.get() + 实际读取记录行数)   ### 用于修改数据库后,再次显示在修改位置
                        IV_上次分页行数.set(实际读取记录行数)                           ### 用于修改数据库后,再次显示在修改位置

def DEF_SQL_查询和显示_定位到编辑处(SQL_CMD, 显编框修改处定位):
    数据库连接对象 = DB_INFO['数据库连接']
    try:
        游标对象 = 数据库连接对象.cursor()                       # 创建一个游标
    except Exception as e:
        ERROR = '创建游标失败' + str(e)
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        #print("创建游标")
        try:
            游标对象.execute(SQL_CMD)
        except Exception as e:
            ERROR = SQL_CMD + '\n' + str(e)
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
        else:
            游标对象_字段名列表 = 游标对象.description
            #print("游标对象_字段名列表", 游标对象_字段名列表)
            字段名列表 = [i[0] for i in 游标对象_字段名列表]
            DB_INFO['字段名列表'] = 字段名列表                   # 保存字段名查询结果
            SV_查询字段列表.set(字段名列表)                      # 展示字段名查询结果
            
            丢弃记录 = 游标对象.fetchmany(显编框修改处定位)      # 先从SQL查询结果中取编辑位置前面叶的内容部分,丢弃

            ## 分页控制
            ## 游标对象.fetchmany(<=0) 和 游标对象.fetchall() 效果一样,为读取全部数据
            分页限制行数 = 分页行数.get()
            if 分页限制行数 > 0:                                 # 分页限制行数 > 0 读取部分记录,可分页显示
                部分记录 = 游标对象.fetchmany(分页限制行数)      # 从SQL查询结果中取指定行数的记录,如果查询结果为空则返回空列表
                实际读取记录行数 = len(部分记录)
                IV_已显示记录数.set(IV_已显示记录数.get() + 实际读取记录行数)   ### 用于修改数据库后,再次显示在修改位置
                IV_上次分页行数.set(实际读取记录行数)                           ### 用于修改数据库后,再次显示在修改位置
                if 部分记录 != []:
                    字段和数据的存储和展示(字段名列表, 部分记录) # 在显编框展示结果,保存结果到全局变量,可以进行修改操作
                    if 实际读取记录行数 < 分页限制行数:          # 已经全部显示,无法分页
                        游标对象.close()                         # 关闭游标对象
                        #print("关闭游标")
                        DB_INFO['数据库游标'] = ''               # 清空数据库游标对象
                        按钮_显编框下一页['state'] = 'disabled'  # 禁止下一页按钮(第一次显示就全部显示完整,不需要下一页按钮)
                    else:
                        DB_INFO['数据库游标'] = 游标对象         # 缓存数据库游标对象
                        按钮_显编框下一页['state'] = 'normal'    # 启用下一页按钮
                else:
                    字段和数据的存储和展示(字段名列表, [])
                    print("空记录")
                    游标对象.close()                             # 关闭游标对象
                    #print("关闭游标")
                    DB_INFO['数据库游标'] = ''                   # 清空数据库游标对象
                    按钮_显编框下一页['state'] = 'disabled'      # 禁止下一页按钮
            else:                                                # 分页限制行数 <= 0 读取全部记录,不分页
                全部记录 = 游标对象.fetchall()                   # 从SQL查询结果中取出全部的记录
                字段和数据的存储和展示(字段名列表, 全部记录)
                游标对象.close()                                 # 关闭游标对象
                #print("关闭游标")
                DB_INFO['数据库游标'] = ''                       # 清空数据库游标对象
                按钮_显编框下一页['state'] = 'disabled'          # 禁止下一页按钮
                实际读取记录行数 = len(全部记录)                                ### 用于修改数据库后,再次显示在修改位置
                IV_已显示记录数.set(IV_已显示记录数.get() + 实际读取记录行数)   ### 用于修改数据库后,再次显示在修改位置
                IV_上次分页行数.set(实际读取记录行数)                           ### 用于修改数据库后,再次显示在修改位置

## 分页操作:返回起始页
def DEF_SQL_查询和显示_返回起始页():
    UPDATE_SELECT()

## 分页操作:显示下一页
## 只有剩余记录 >= 0 时才能按下一页按钮(由上一步控制)
def DEF_SQL_查询和显示_下一页():
    游标对象 = DB_INFO['数据库游标']                                # 获取缓存的数据库游标对象
    if 游标对象 == '':
        ERROR = "DB_INFO['数据库游标'] == '' 数据库查询游标未缓存"
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        print("再次使用同一个游标对象读取后续数据", 游标对象)
        字段名列表 = DB_INFO['字段名列表']                          # 从全局变量中取出保存的字段名信息
        分页限制行数 = 分页行数.get()
        if 分页限制行数 <= 0:
            WARNING = '为防止一次显示数据过多,请务必设置分页行数(分页显示行数>=0)'
            tkinter.messagebox.showwarning(title='WARNING', message=WARNING)
        else:
            剩余记录数量 = IV_记录总行数.get() - IV_已显示记录数.get()
            print("剩余记录数量", 剩余记录数量, "分页限制行数", 分页限制行数)
            if 分页限制行数 >= 剩余记录数量:
                print("显示最后一页")
                部分记录 = 游标对象.fetchmany(分页限制行数)
                字段和数据的存储和展示(字段名列表, 部分记录)
                游标对象.close()                                                ## 关闭游标对象
                print("关闭游标")
                DB_INFO['数据库游标'] = ''                                      ## 清空数据库游标对象
                按钮_显编框下一页['state'] = 'disabled'                         ## 禁止下一页按钮
                IV_已显示记录数.set(IV_已显示记录数.get() + 剩余记录数量)   ### 用于修改数据库后,再次显示在修改位置
                IV_上次分页行数.set(剩余记录数量)                           ### 用于修改数据库后,再次显示在修改位置
            else:
                print("保持游标(还未到底)")
                部分记录 = 游标对象.fetchmany(分页限制行数)
                字段和数据的存储和展示(字段名列表, 部分记录)
                IV_已显示记录数.set(IV_已显示记录数.get() + 分页限制行数)   ### 用于修改数据库后,再次显示在修改位置
                IV_上次分页行数.set(分页限制行数)                           ### 用于修改数据库后,再次显示在修改位置

## 非查询的SQL语句(执行一条SQL语句)(每次执行都要打开关闭游标)
def DEF_SQL_执行(SQL_CMD):
    数据库连接对象 = DB_INFO['数据库连接']
    try:
        游标对象 = 数据库连接对象.cursor()    # 创建一个游标
    except Exception as e:
        ERROR = '创建游标失败' + str(e)
        return(1, ERROR)
    else:
        #print("创建游标")
        try:
            游标对象.execute(SQL_CMD)
        except Exception as e:
            ERROR = str(e)
            ##失败情况不关闭游标,以免点击下一页失效
            return(1, ERROR)
        else:
            数据库连接对象mit()                # 提交更改
            受影响行数 = 游标对象.rowcount         # 获取受影响行数
            print("受影响行数", 受影响行数)
            游标对象.close()
            #print("关闭游标")
            DB_INFO['数据库游标'] = ''
            return(0, 受影响行数)

## 清空框内控件
def FRAME_CLEAR(FRAME_NAME):
    for X in FRAME_NAME.winfo_children():
        X.destroy()

## 在显编框展示结果,保存结果到全局变量,可以进行修改操作
def 字段和数据的存储和展示(L, LL):
    列数 = len(L)
    行数 = len(LL)
    
    ## 记录字段中每个字段值的字符数量
    #L_列最大字符数 = [len(i) for i in L]          # 字段列表中每个字段有多少个字符(中文1个字当做1字符)
    L_列最大字符数 = [len(i.encode()) for i in L] # 字段列表中每个字段有多少个字符(中文1个字当做多个字符)

    ## 遍历每一行数据,记录每列字符数量最多值
    for i in LL:
        for n in range(0, len(i)):
            if i[n] == None:        # 空值
                len_x = 4           # 以4字符作为结果
            else:
                #len_x = len(str(i[n]))           # 数据列表中每列有有多少个字符(中文1个字当做1字符)
                len_x = len(str(i[n]).encode())   # 数据列表中每列有有多少个字符(中文1个字当做多字符)
            ## 比较,取最大值
            if L_列最大字符数[n] >= len_x:
                pass
            else:
                L_列最大字符数[n] = len_x
    #print("L_列最大字符数", L_列最大字符数)
    
    单元格宽度限制 = IV_单元格限宽.get()              # 单元格宽度限制字符数
    #L_列最大字符数_修正 = [单元格宽度限制 if i > 单元格宽度限制 else i for i in L_列最大字符数]
    L_列最大字符数_修正 = [单元格宽度限制 if i > 单元格宽度限制 else math.ceil(i*1.2) for i in L_列最大字符数] # 太宽的字符会不够放如大写A,每个字符扩大20%,合计向上取整
    #print("L_列最大字符数_修正", L_列最大字符数_修正)
    
    FRAME_CLEAR(LabelFrame_显编框)                    # 清空框内控件
    
    ## 创建画布
    画布 = Canvas(LabelFrame_显编框, 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')
    
    ## 动态设置画布窗口宽高:根据主主窗口的参数设置限宽限高
    主窗口大小和位置 = top.geometry()
    主窗口宽, 主窗口高, 主窗口X, 主窗口Y = re.findall('[0-9]+', 主窗口大小和位置)
    画布最大宽 = int(主窗口宽) -310       # 减去左边框和中间分隔框的宽
    画布最小宽 = 589                      # 保障最小宽度
    画布最大高 = int(主窗口高) -500
    画布最小高 = 250                      # 保障最小高度
    
    ## 设置画布参数
    总行数 = 行数 + 1    # 数据行数n + 字段行数1
    
    ## 画布可滚动显示的最大宽和高(要刚好能放下画布里的Frame里的全部控件)
    ## 一个单元格(默认宽为144像素,20字符+4像素打底),实测可以放20个数字(7) 20小写字母(7) 17.5大写字母A(8) 11.5个汉字(12)
    ## Entry 字符宽度与像素关系:
    # Entry 最小宽度字符为1(设置为0也会自动设置为1)
    # 宽度 = 1字符 = 11像素(1*7 +4)
    # 宽度 = 2字符 = 18像素(2x7 +4)
    # 宽度 = 3字符 = 25像素(3x7 +4)
    画布滚动最右边 = sum(L_列最大字符数_修正)*7 + 列数*4 +2   # 总字符数量x9像素+每个单元格需要初始4像素+2像素留底表示后面已经没有了
    #print("画布滚动最右边", 画布滚动最右边)
    
    # Entry 默认高度为 21像素(20像素+1分隔像素)
    画布滚动最下边 = 21*总行数
    
    ## 滚动能到的宽高必须>=元素总和宽高像素,不然会显示不出
    画布['scrollregion'] = (0,0,画布滚动最右边,画布滚动最下边)   # 一个元组 tuple (w, n, e, s) ,定义了画布可滚动的最大区域,w 为左边,n 为头部,e 为右边,s 为底部
    
    
    ## 动态设置显示画布固定显示宽和高(要和主显示框的大小匹配)
    if 画布滚动最右边 < 画布最大宽:         # 如果当前全部元素像素宽之和比画布可以给的最大宽小
        画布['width'] = 画布滚动最右边      # 使用实际需要的宽度即可
    else:                                   # 否则
        if 画布最大宽 < 画布最小宽:
            画布['width'] = 画布最小宽
        else:
            画布['width'] = 画布最大宽      # 画布使用最大宽度,内容超出部分使用滑动显示

    if 画布滚动最下边 < 画布最大高:
        画布['height'] = 画布滚动最下边
    else:
        if 画布最大高 < 画布最小高:
            画布['height'] = 画布最小高
        else:
            画布['height'] = 画布最大高

    # 竖滚动条
    Scrollbar_画布_竖 = Scrollbar(LabelFrame_显编框, command=画布.yview)
    Scrollbar_画布_竖.grid(row=0,column=1,sticky=S+W+E+N)
    
    # 横滚动条
    Scrollbar_画布_横 = Scrollbar(LabelFrame_显编框, command=画布.xview, orient=HORIZONTAL)
    Scrollbar_画布_横.grid(row=1,column=0,sticky=S+W+E+N)

    画布.config(xscrollcommand=Scrollbar_画布_横.set, yscrollcommand=Scrollbar_画布_竖.set) # 自动设置滚动幅度
    画布.create_window((0,0), window=画布Frame框, anchor='nw')

    ## 在画布里的Frame里创建控件
    # 清除全局字典的内容
    字典_查询字段_坐标_对象.clear()
    字典_查询字段_坐标_初值.clear()
    字典_查询结果_坐标_对象.clear()
    字典_查询结果_坐标_初值.clear()
    
    ## 字段名
    for 列 in range(0, 列数):
        #print("L_列最大字符数[列]", L_列最大字符数[列])
        #print("L_列最大字符数_修正", L_列最大字符数_修正[列])
        初始值 = 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, 列数):
            初始值 = str(LL[行][列])                                                                    # 转成字符串
            字典_查询结果_坐标_对象[(行,列)] = Entry(数据框, width=L_列最大字符数_修正[列])             # 控件对象放到指定框内,并保存对象到对象字典中
            字典_查询结果_坐标_初值[(行,列)] = 初始值                                                   # 保存初始值
            字典_查询结果_坐标_对象[(行,列)].insert(0, 初始值)                                          # 写入Entry作为初始值(从标签内开头开始填充新值,因为是全新标签,所有不用先删除里面内容)
            字典_查询结果_坐标_对象[(行,列)].grid(row=行,column=列,sticky='W')                          # Entry排放到指定位置
            字典_查询结果_坐标_对象[(行,列)].bind("<Button-1>", 左键单击)                               # 每个控件对象都绑定左键单击事件
            字典_查询结果_坐标_对象[(行,列)].bind("<Button-3>", DEF_弹出_数据框_右键菜单)               # 每个控件对象都绑定右键菜单事件

## 查询数据库表,只为更新数据表框内数据表列表
def DEF_查询数据库表():
    SQL_CMD = 'SHOW TABLES;'     # MySQL查表命令
    R = DEF_SQL_查询和返回(SQL_CMD)
    if R[0] == 0:
        查询结果 = R[1]
        if 查询结果 == []:
            STR_数据表列表内容.set('<空>')
        else:
            数据表列表 = [i[0] for i in 查询结果]    # [('sqlite_sequence',), ('t0',), ('t1',)]
            STR_数据表列表内容.set(数据表列表)
    else:
        ERROR = 'DEF_SQL_查询和返回() 失败,错误:' + R[1]
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)





##############
## 按钮函数 ##
##############

## 事件函数:双击列表中的数据表名查询表内全部记录
def DEF_双击表名(event):
    当前选择 = Listbox_数据表列表.curselection()                         # 列表数据定位 (序号数,)
    当前选择值 = Listbox_数据表列表.get(当前选择)                        # 对应的值
    print("DEF_双击表名 当前选择", 当前选择, "当前选择值", 当前选择值)   # 如:当前选择 (0,) 当前选择值 001
    if 当前选择值 != '<空>':
        DB_TABLE_NAME.set(当前选择值)                                    # 更新表名变量
        ## 打开数据表(查询表内容)
        SQL_CMD = f'SELECT * FROM {当前选择值}'
        DEF_SQL_查询和显示(SQL_CMD)
    else:
        print("<空> 是数据库内无数据表的提示,忽略")

## 返回起始页
def DEF_按钮_显编框起始页():
    print("DEF_按钮_显编框起始页")
    DEF_SQL_查询和显示_返回起始页()

## 下一页
def DEF_按钮_显编框下一页():
    print("DEF_按钮_显编框下一页")
    DEF_SQL_查询和显示_下一页()

## 修改记录
def DEF_按钮_确认修改数据库():
    数据表名 = DB_TABLE_NAME.get()
    字段名列表 = DB_INFO['字段名列表']
    ## 找主键名:查数据库表得到主键信息 L_PK_NAME
    R = 主键名列表(数据表名)
    if R[0] == 0:
        L_PK_NAME = R[1]
        主键数量 = len(L_PK_NAME)
        if 主键数量 == 0:
            ERROR = f'数据表"{数据表名}"没有主键,为防止误操作,本次不执行修改。'
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
        else:
            ## 一个或多个主键的情况
            列表_主键信息 = []                                           # 查询显示结果中的主键字段名信息列表
            for 主键名 in L_PK_NAME:
                for 列号 in range(0, len(字段名列表)):                   # 按下标遍历查询显示结果中每个字段名
                    if 字段名列表[列号] == 主键名:                       # 如果含有主键字段
                        主键信息 = (列号,主键名)                         # 记录在查询显示结果中的下标和对应主键字段名
                        列表_主键信息.append(主键信息)                   # 添加到主键字段名信息列表,[(列号1,主键名1), (列号2,主键名2)]
            print("显编框内 列表_主键信息", 列表_主键信息)
            if 列表_主键信息 == []:
                ERROR = f'主键字段{L_PK_NAME}未包含在当前查询结果中,当前查询字段列表{字段名列表},为防止误操作,本次不执行修改。'
                tkinter.messagebox.showerror(title='ERROR', message=ERROR)
            elif len(列表_主键信息) != 主键数量:
                ERROR = f'主键字段{L_PK_NAME}未全部包含在当前查询结果中,当前查询字段列表{字段名列表},为防止误操作,本次不执行修改。'
                tkinter.messagebox.showerror(title='ERROR', message=ERROR)
            else:
                ## 主键大于等于1且都在显编框内
                #编辑信息字典 = {'当前行WHERE限定语句':[(字段名,新值),(字段名,新值)]}
                编辑信息字典 = {}
                for i in 字典_查询结果_坐标_对象:                   # 遍历编辑框(查询结果框)中的全部控件
                    控件旧值 = 字典_查询结果_坐标_初值[i]
                    控件新值 = 字典_查询结果_坐标_对象[i].get()
                    if 控件旧值 != 控件新值:                        # 当原值和当前值不一致,说明此控件值被修改
                        行号,列号 = i                               # 提取当前控件的坐标
                        字段名 = 字典_查询字段_坐标_初值[(0,列号)]  # 字段名存储在字段全局变量中
                        字段值 = 控件新值
                        SQL_条件限定句 = ''                         # 拼接WHERE限定语句,同行限定语句一样,限定语句作为key合并同行修改内容
                        L_KEY = []
                        for 主键信息 in 列表_主键信息:
                            主键列号 = 主键信息[0]
                            主键名 = 主键信息[1]
                            主键值 = 字典_查询结果_坐标_初值[(行号,主键列号)]
                            L_KEY.append((主键名,主键值))
                        print("L_KEY", L_KEY)                       # L_KEY [('A', '1.0'), ('B', '1')]
                        for j in range(0, len(L_KEY)):
                            (子主键名, 子主键值) = L_KEY[j]
                            if j == 0:
                                SQL_条件限定句 += f' WHERE {子主键名}="{子主键值}"'
                            else:
                                SQL_条件限定句 += f' AND {子主键名}="{子主键值}"'
                        print("SQL_条件限定句", SQL_条件限定句)
                        ## 把同行的修改信息合并在一起,方便整合成一条修改语句
                        if SQL_条件限定句 not in 编辑信息字典:
                            编辑信息字典[SQL_条件限定句] = [(字段名,字段值)]
                        else:
                            编辑信息字典[SQL_条件限定句].append((字段名,字段值))
                print("编辑信息字典", 编辑信息字典)                 # 编辑信息字典 {' WHERE A="1.0" AND B="1"': [('B', '10')], ' WHERE A="1.0" AND B="2"': [('B', '20'), ('X', '0:00:0100')]}
                ## 根据 编辑信息字典 制作数据库语句
                L_SQL_CMD = []
                for K in 编辑信息字典:
                    #print("K SQL_条件限定句", K, "V [(字段名,新值),(字段名,新值)]", 编辑信息字典[K])
                    SQL_WHERE语句 = K
                    修改字段列表 = 编辑信息字典[K]
                    修改字段数量 = len(修改字段列表)
                    S = ''
                    for i in range(0, 修改字段数量):
                        字段名,字段值 = 修改字段列表[i]
                        if i == 0:
                            S += f'{字段名} = "{字段值}"'
                        else:
                            S += f', {字段名} = "{字段值}"'
                    SQL_CMD = f'UPDATE {数据表名} SET {S} {SQL_WHERE语句}'
                    print("SQL_CMD", SQL_CMD)
                    L_SQL_CMD.append(SQL_CMD)
                print("L_SQL_CMD", L_SQL_CMD)
                if L_SQL_CMD == []:
                    WARNING = '没有可以修改的内容'
                    #print(WARNING)
                    按钮_确认修改数据库.grid_forget()               # 隐藏 按钮_确认修改数据库
                    tkinter.messagebox.showwarning(title='WARNING', message=WARNING)
                else:
                    ## 逐个执行全部SQL语句,失败的跳过
                    成功列表 = []
                    失败列表 = []
                    for i in L_SQL_CMD:
                        RR = DEF_SQL_执行(i)
                        if RR[0] == 0:
                            受影响行数 = RR[1]
                            INFO = f'数据库 {DB_NAME.get()} 执行SQL语句 {i} 成功,受影响行数:{受影响行数}'
                            TEXT_数据库变动日志.insert(0.0, INFO+'\n')
                            Log.info(INFO)
                            成功列表.append(INFO)
                        else:
                            ERROR = f'数据库 {DB_NAME.get()} 执行SQL语句 {i} 失败 {RR[1]}'
                            失败列表.append(ERROR)
                    print("成功列表", 成功列表)
                    print("失败列表", 失败列表)
                    if 失败列表 != []:
                        SHOW_STR = ''
                        for i in 成功列表:
                            SHOW_STR += i + '\n'
                        for i in 失败列表:
                            SHOW_STR += i + '\n'
                        tkinter.messagebox.showerror(title='ERROR', message=SHOW_STR)
                    ## 执行过SQL就更新
                    按钮_确认修改数据库.grid_forget()           # 隐藏 按钮_确认修改数据库
                    ###UPDATE_SELECT()                          # 刷新显编框:从头显示
                    UPDATE_SELECT_LINIT()                       # 刷新显编框:定位到修改处
    else:
        ERROR = f'查询数据表"{数据表名}"主键信息失败:{R[1]}'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)

## 清除命令框里的内容
def DEF_按钮_清屏():
    文本框_命令行.delete(0.0, END)
    文本框_命令行.focus_set()



###########################
## 创建新窗口 新建数据表 ##
###########################
def 创建数据表窗口_确定(窗口对象):
    print("确定")
    数据库引擎 = 新表数据库引擎.get()
    字符集 = 新表字符集.get()
    新表名 = 新建数据表名.get()
    if 新表名 == '':
        print("没有新建表名")
    else:
        ## 字段名不能为空,不能重复
        错误标记 = 0
        测试字段名列表 = []
        for K in 字典_创建表_字段信息:
            测试字段名 = 字典_创建表_字段信息[K][2].get().strip()
            if 测试字段名 == '':
                错误标记 = 1
                print("含有空字段名")
                break
            else:
                测试字段名列表.append(测试字段名)
        if len(测试字段名列表) != len(set(测试字段名列表)):
            错误标记 = 1
            print("有重复字段名")
        
        if 错误标记 == 0:
            全部字段信息列表 = []
            主键字段名列表 = []
            for 字段编号 in 字典_创建表_字段信息:
                字段对象列表 = 字典_创建表_字段信息[字段编号]
                字段名 = 字段对象列表[2].get()
                字段类型 = 字段对象列表[3].get()
                是否可空 = 字段对象列表[4].get()
                默认值  = 字段对象列表[5].get()
                是否主键 = 字段对象列表[6].get()
                if 默认值 != '':
                    if 默认值 == '自增数字':
                        默认值 = 'auto_increment'
                    else:
                        默认值 = f'DEFAULT {默认值}'
                
                if 是否主键 == '是':
                    主键字段名列表.append(字段名)
                
                单条字段信息 = f'{字段名} {字段类型} {是否可空} {默认值}'
                print("单条字段信息", 单条字段信息)
                全部字段信息列表.append(单条字段信息)
            ## 新建表
            SQL_CMD = f'CREATE TABLE {新表名}( \n'             ## 表名              
            ## 字段
            for i in range(0, len(全部字段信息列表)):
                字段信息 = 全部字段信息列表[i]
                if i == 0:
                    SQL_CMD += 字段信息
                else:
                    SQL_CMD += ',' + 字段信息
            ## 设置主键
            主键字段信息 = ''
            for i in range(0, len(主键字段名列表)):
                if i == 0:
                    主键字段信息 += 主键字段名列表[i]
                else:
                    主键字段信息 += ',' + 主键字段名列表[i]
            if 主键字段信息 == '':
                print("警告:没有设置主键")
                SQL_CMD += ')'
            else:
                SQL_CMD += f', PRIMARY KEY ({主键字段信息})) ' ## 主键
            SQL_CMD += f'ENGINE={数据库引擎} '
            SQL_CMD += f'DEFAULT CHARSET={字符集};'            ## 字符集
            print("SQL_CMD", SQL_CMD)
            R = DEF_SQL_执行(SQL_CMD)
            if R[0] == 0:               # 创建新表成功
                窗口对象.withdraw()     # 关闭编辑窗口
                DEF_查询数据库表()      # 重新查询数据库表
            else:
                print("提示错误")
                ERROR = SQL_CMD + '\n' + R[1]
                tkinter.messagebox.showerror(title='ERROR', message=ERROR)

def 创建数据表窗口_取消(窗口对象):
    print("取消")
    窗口对象.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)
    Combobox_字段类型['value'] = ('INTEGER','CHAR()','VARCHAR()','TEXT','DATE','TIME','TINYINT','SMALLINT','MEDIUMINT','BIGINT','FLOAT','DOUBLE','DECIMAL','YEAR','DATETIME','TIMESTAMP','TINYBLOB','TINYTEXT','BLOB','MEDIUMBLOB','MEDIUMTEXT','LONGBLOB','LONGTEXT')
    Combobox_是否可空 = ttk.Combobox(显示框, width=10)
    Combobox_是否可空['value'] = ('NULL', 'NOT NULL')
    Combobox_是否可空.current(0)                                                # 默认值中的内容为索引,从0开始
    Combobox_默认值 = ttk.Combobox(显示框)
    Combobox_默认值['value'] = ('自增数字', 'CURRENT_TIMESTAMP')
    Combobox_是否主键 = ttk.Combobox(显示框, width=15)
    Combobox_是否主键['value'] = ('是', '否')
    Combobox_是否主键.current(1)
    列表_字段属性对象 = [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(新窗口)
    按钮框 = 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)
    
    ## 表名框:用户输入新建的表名
    Label(表名框, text='[新建数据表名]').grid(row=0,column=0,sticky='NW')
    Entry(表名框, textvariable=新建数据表名).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')
    新表数据库引擎.set(Combobox_数据库引擎.get())            # 设置全局变量为框内值
    def Combobox_数据库引擎选择后执行函数(event):
        新表数据库引擎.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')
    新表字符集.set(Combobox_字符集.get())
    def Combobox_字符集选择后执行函数(event):
        新表字符集.set(Combobox_字符集.get())
    Combobox_字符集.bind('<<ComboboxSelected>>', Combobox_字符集选择后执行函数)
    
    ## 标题框:提示每列含义
    Label(标题框, text='删',           width=2,  bg='#00BFFF').grid(row=2, column=0, sticky='NW')
    Label(标题框, text='序',           width=2,  bg='#00BFFF').grid(row=2, column=1, sticky='NW')
    Label(标题框, text='列名(字段名)', width=20, bg='#00BFFF').grid(row=2, column=2, sticky='NW')
    Label(标题框, text='类型',         width=17, bg='#00BFFF').grid(row=2, column=3, sticky='NW')
    Label(标题框, text='是否允许空',   width=12, bg='#00BFFF').grid(row=2, column=4, sticky='NW')
    Label(标题框, text='默认值',       width=23, bg='#00BFFF').grid(row=2, column=5, sticky='NW')
    Label(标题框, text='主键标识',     width=17, bg='#00BFFF').grid(row=2, column=6, sticky='NW')

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



#############################
## 创建新窗口 修改数据表名 ##
#############################
def 修改表名窗口_确定(窗口对象):
    #print("修改表名窗口_确定")
    原表名 = DB_TABLE_NAME.get()
    if 原表名 == '':
        ERROR = '获取原表名失败'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        新表名 = 字典_新数据表表名对象['新数据表名输入框对象'].get().strip()
        if 新表名 == '':
            ERROR = '无效新表名'
            #print(ERROR)
            SV_修改表名窗口错误提示.set(ERROR)
        else:
            SQL_CMD = f'ALTER TABLE {原表名} RENAME {新表名}'
            R = DEF_SQL_执行(SQL_CMD)
            if R[0] == 0:
                #print("修改表名成功")
                受影响行数 = R[1]
                INFO = f'执行SQL语句 {SQL_CMD} 成功,受影响行数:{受影响行数}'
                TEXT_数据库变动日志.insert(0.0, INFO+'\n')     # 日志框打印
                Log.info(INFO)                                 # 写入日志文件
                窗口对象.withdraw()                            # 关闭编辑窗口
                DEF_查询数据库表()                             # 重新查询数据库表
                SV_修改表名窗口错误提示.set('')
            else:
                #print("修改表名失败")
                ERROR = SQL_CMD + '\n' + R[1]
                SV_修改表名窗口错误提示.set(ERROR)

def 修改表名窗口_取消(窗口对象):
    print("取消")
    窗口对象.withdraw()

def DEF_弹出修改表名窗口():
    原数据表名 = DB_TABLE_NAME.get()
    字典_新数据表表名对象.clear()
    新窗口 = Toplevel()
    新窗口.title('修改数据表名窗口')
    显示坐标 = f'+{屏幕宽//2-300}+{屏幕高//2-100}'
    新窗口.geometry(显示坐标)
    
    提示框 = Frame(新窗口)
    数据框 = Frame(新窗口)
    按钮框 = Frame(新窗口)
    提示框.grid(row=0,column=0)
    数据框.grid(row=1,column=0)
    按钮框.grid(row=2,column=0)

    ## 提示框
    Label(提示框, textvariable=SV_修改表名窗口错误提示).grid(row=0,column=0,sticky='NW')
    
    ## 数据框
    Label(数据框, text='原数据表名').grid(row=0,column=0,sticky='NW')    #00
    Label(数据框, text=原数据表名).grid(row=0,column=1,sticky='NW')      #01
    Label(数据框, text='新数据表名').grid(row=1,column=0,sticky='NW')    #10
    新数据表名 = Entry(数据框)
    新数据表名.grid(row=1,column=1,sticky='NW')                          #11
    字典_新数据表表名对象['新数据表名输入框对象'] = 新数据表名
    
    ## 按钮框
    确定按钮 = Button(按钮框, text='确定', command=lambda 窗口对象=新窗口:修改表名窗口_确定(窗口对象))
    确定按钮.grid(row=1,column=0)
    取消按钮 = Button(按钮框, text='取消', command=lambda 窗口对象=新窗口:修改表名窗口_取消(窗口对象))
    取消按钮.grid(row=1,column=1)



#########################
## 创建新窗口 新加字段 ##
#########################
def 新加字段窗口_确定(窗口对象):
    print("新加字段窗口_确定")
    数据表名 = DB_TABLE_NAME.get()
    if 数据表名 == '':
        print("没有表名")
    else:
        ## 字段名不能为空,不能重复
        错误标记 = 0
        测试字段名列表 = []
        for K in 字典_新加字段信息:
            测试字段名 = 字典_新加字段信息[K][2].get().strip()
            if 测试字段名 == '':
                错误标记 = 1
                print("含有空字段名")
                break
            else:
                测试字段名列表.append(测试字段名)
        if len(测试字段名列表) != len(set(测试字段名列表)):
            错误标记 = 1
            print("有重复字段名")

        if 错误标记 == 0:
            全部新增字段信息列表 = []
            for 字段编号 in 字典_新加字段信息:
                单条新增字段信息 = []
                字段对象列表 = 字典_新加字段信息[字段编号]
                字段名 = 字段对象列表[2].get()
                字段类型 = 字段对象列表[3].get()
                是否可空 = 字段对象列表[4].get()
                默认值  = 字段对象列表[5].get()
                是否主键 = 字段对象列表[6].get()
                ## 判断默认值情况
                if 默认值 != '':
                    if 默认值 == '自增数字':
                        默认值 = 'auto_increment'
                    else:
                        默认值 = f'DEFAULT {默认值}'
                ## 判断主键情况
                if 是否主键 == '是':
                    主键标识 = 'PRIMARY KEY'
                else:
                    主键标识 = ''
                ## 拼接新增字段的SQL语句
                单条新增字段信息 = f'ALTER TABLE {数据表名} ADD {字段名} {字段类型} {是否可空} {默认值} {主键标识}'
                print("单条新增字段信息", 单条新增字段信息)
                全部新增字段信息列表.append(单条新增字段信息)
            ## 逐个执行全部SQL语句,失败的跳过
            成功列表 = []
            失败列表 = []
            for i in 全部新增字段信息列表:
                RR = DEF_SQL_执行(i)
                if RR[0] == 0:
                    受影响行数 = RR[1]
                    INFO = f'数据库 {DB_NAME.get()} 执行SQL语句 {i} 成功,受影响行数:{受影响行数}'
                    TEXT_数据库变动日志.insert(0.0, INFO+'\n')
                    Log.info(INFO)
                    成功列表.append(INFO)
                else:
                    ERROR = f'数据库 {DB_NAME.get()} 执行SQL语句 {i} 失败 {RR[1]}'
                    失败列表.append(ERROR)
            print("成功列表", 成功列表)
            print("失败列表", 失败列表)
            ## 执行过SQL就更新
            UPDATE_SELECT()                          # 刷新显编框:从头显示
            if 失败列表 != []:
                SHOW_STR = ''
                for i in 成功列表:
                    SHOW_STR += i + '\n'
                for i in 失败列表:
                    SHOW_STR += i + '\n'
                tkinter.messagebox.showerror(title='ERROR', message=SHOW_STR)
            else:
                ## 创建新字段全部成功
                窗口对象.withdraw()     # 关闭编辑窗口

def 新加字段窗口_取消(窗口对象):
    print("取消")
    窗口对象.withdraw()

def 新加字段窗口_增加字段(显示框):
    print("增加字段")
    行号列表 = [i for i in 字典_新加字段信息]
    print("行号列表", 行号列表)
    if 行号列表 == []:
        新行号 = 0
    else:
        最大行号 = max(行号列表)
        print("上次最大行号", 最大行号)
        新行号 = 最大行号 + 1
        print("当前最大行号", 新行号)
    
    # 字段信息编号和删除字段信息按钮
    字段编号 = 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)
    Combobox_字段类型['value'] = ('INTEGER','CHAR()','VARCHAR()','TEXT','DATE','TIME','TINYINT','SMALLINT','MEDIUMINT','BIGINT','FLOAT','DOUBLE','DECIMAL','YEAR','DATETIME','TIMESTAMP','TINYBLOB','TINYTEXT','BLOB','MEDIUMBLOB','MEDIUMTEXT','LONGBLOB','LONGTEXT')
    Combobox_是否可空 = ttk.Combobox(显示框, width=10)
    Combobox_是否可空['value'] = ('NULL', 'NOT NULL')
    Combobox_是否可空.current(0)                                                # 默认值中的内容为索引,从0开始
    Combobox_默认值 = ttk.Combobox(显示框)
    Combobox_默认值['value'] = ('自增数字', 'CURRENT_TIMESTAMP')
    Combobox_是否主键 = ttk.Combobox(显示框, width=15)
    Combobox_是否主键['value'] = ('是', '否')
    列表_字段属性对象 = [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(新窗口)
    按钮框 = 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)
    
    ## 表名框:显示当前操作的表名
    Label(表名框, text='数据表名: ').grid(row=0,column=0,sticky='NW')
    数据表名 = DB_TABLE_NAME.get()
    数据表名显示 = Entry(表名框)
    数据表名显示.grid(row=0,column=1,sticky='NW')
    数据表名显示.insert(0, 数据表名)
    数据表名显示['state'] = 'readonly'
    
    ## 标题框:提示每列含义
    Label(标题框, text='删',           width=2,  bg='#00BFFF').grid(row=2, column=0, sticky='NW')
    Label(标题框, text='序',           width=2,  bg='#00BFFF').grid(row=2, column=1, sticky='NW')
    Label(标题框, text='列名(字段名)', width=20, bg='#00BFFF').grid(row=2, column=2, sticky='NW')
    Label(标题框, text='类型',         width=17, bg='#00BFFF').grid(row=2, column=3, sticky='NW')
    Label(标题框, text='是否允许空',   width=12, bg='#00BFFF').grid(row=2, column=4, sticky='NW')
    Label(标题框, text='默认值',       width=23, bg='#00BFFF').grid(row=2, column=5, sticky='NW')
    Label(标题框, text='主键标识',     width=17, bg='#00BFFF').grid(row=2, column=6, sticky='NW')

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



#########################
## 创建新窗口 修改字段 ##
#########################
字典_修改字段信息对象 = {}    # 
字典_原字段信息 = {}          # {'COLUMN_DEFAULT':'', 'IS_NULLABLE':'', 'COLUMN_TYPE':'', 'COLUMN_KEY':''}

def 修改字段窗口_确定(窗口对象):
    print("修改字段窗口_确定")
    表名 = DB_TABLE_NAME.get()
    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 = '删除主键还是手动命令操作吧 ALTER TABLE 表名 DROP PRIMARY KEY'
                tkinter.messagebox.showwarning(title='WARNING', message=WARNING)
            else:
                if 新字段名 == 原字段名:
                    ## 修改字段属性的SQL语句
                    ## ALTER TABLE 表名 MODIFY 字段名称 字段类型 [完整性约束条件]
                    SQL_CMD = f'ALTER TABLE {表名} MODIFY {原字段名} {新字段类型} {新是否可空} {新默认值} {主键标识}'
                else:
                    ## 修改字段名和字段属性
                    ## ALTER TABLE 表名 CHANGE 原字段名 新字段名 字段类型 约束条件
                    SQL_CMD = f'ALTER TABLE {表名} CHANGE {原字段名} {新字段名} {新字段类型} {新是否可空} {新默认值} {主键标识}'
                print("SQL_CMD", SQL_CMD)
                R = DEF_SQL_执行(SQL_CMD)
                print(R)
                if R[0] == 0:                                      # 创建新字段全部成功
                    受影响行数 = R[1]
                    INFO = f'执行SQL语句 {SQL_CMD} 成功,受影响行数:{受影响行数}'
                    TEXT_数据库变动日志.insert(0.0, INFO+'\n')     # 日志框打印
                    Log.info(INFO)                                 # 写入日志文件
                    窗口对象.withdraw()                            # 关闭编辑窗口
                    UPDATE_SELECT()                                # 成功后,更新显示表格
                else:
                    ERROR = R[1]
                    tkinter.messagebox.showerror(title='ERROR', message=ERROR)

def 修改字段窗口_取消(窗口对象):
    print("取消")
    窗口对象.withdraw()

def DEF_弹出修改字段窗口():
    字段索引 = 字段框_定位列.get()
    原字段名 = 字典_查询字段_坐标_初值[(0,字段索引)]
    ## 查询数据库,取原字段信息
    数据表名 = DB_TABLE_NAME.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)
            Combobox_字段类型['value'] = ('INTEGER','CHAR()','VARCHAR()','TEXT','DATE','TIME','TINYINT','SMALLINT','MEDIUMINT','BIGINT','FLOAT','DOUBLE','DECIMAL','YEAR','DATETIME','TIMESTAMP','TINYBLOB','TINYTEXT','BLOB','MEDIUMBLOB','MEDIUMTEXT','LONGBLOB','LONGTEXT')
            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 大文本窗口_确定(窗口对象):
    print("大文本编辑_确认")
    ## 获取源控件定位
    行 = 数据框_定位行.get()
    列 = 数据框_定位列.get()
    ## 提取源控件原值
    原值 = 字典_查询结果_坐标_初值[(行,列)]
    print("原值", 原值)
    ## 提取编辑后的新值
    新值 = 字典_对象存储['文本编辑对象'].get(0.0, END).rstrip('\n')      # insert 时候会多个换行,麻烦,直接删除
    现值 = 字典_查询结果_坐标_对象[(行,列)].get()                        # Entry控件是可以输入的,此处用于处理从其他值改回原值的情况
    print("用户编辑后新值", 新值)
    if 新值 != 原值:
        print("有变化")
        字典_查询结果_坐标_对象[(行,列)].delete(0, END)     # 删除原内容
        字典_查询结果_坐标_对象[(行,列)].insert(0, 新值)    # 写入新内容
        ## 改变颜色,有变化用绿色
        字典_查询结果_坐标_对象[(行,列)]['bg'] = '#7FFF00'
        ## 显示修改数据库的按钮
        按钮_确认修改数据库.grid()
    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(显示坐标)

    小文本框 = Frame(新窗口)
    大文本框 = Frame(新窗口)
    按钮框 = Frame(新窗口)
    小文本框.grid(row=0,column=0,sticky='NW')
    大文本框.grid(row=1,column=0,sticky='NW')
    按钮框.grid(row=2,column=0)
    
    ## 小文本框
    Label(小文本框, text='[现值]').grid(row=0,column=0,sticky='W')
    Entry_原值 = Entry(小文本框, width=80)
    Entry_原值.grid(row=0,column=1,sticky='W')
    Entry_原值.insert(0, 单元格现值)
    
    Label(小文本框, text='[原值]').grid(row=1,column=0,sticky='W')
    Entry_原值 = Entry(小文本框, width=80)
    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)
    Scrollbar_编辑子框_竖.grid(row=0, column=1, sticky=S+W+E+N)
    Scrollbar_编辑子框_横.grid(row=1, column=0, sticky=S+W+E+N)

    ## 按钮框
    确定按钮 = Button(按钮框, text='确定', command=lambda 窗口对象=新窗口:大文本窗口_确定(窗口对象))
    确定按钮.grid(row=1,column=0)
    取消按钮 = Button(按钮框, text='取消', command=lambda 窗口对象=新窗口:大文本窗口_取消(窗口对象))
    取消按钮.grid(row=1,column=1)



###########################
## 创建新窗口 添加新记录 ##
###########################
def DEF_新增记录():
    数据表名 = DB_TABLE_NAME.get()
    R = DEF_TABLE_COLUMN_NAME(数据表名)         # 查询数据表的字段信息
    if R[0] == 0:
        字段名列表 = R[1]
        DEF_弹出新加记录窗口(字段名列表)
    else:
        ERROR = R[1]
        print(ERROR)
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)

def 新加记录窗口_确定(窗口对象):
    print("确定")
    数据表名 = DB_TABLE_NAME.get()
    ## 查找有值的字段,拼成INSERT SQL语句
    LT = []                                         # 插入信息列表,元素是元组,元组元素是要插入的(字段名,字段值)
    字段数量 = len(字典_添加记录_坐标_初值)//2      # 添加记录框总是显示2行,第一行为字段名,第二行初始全为空,字段数量=2行总格子数的一半
    for i in range(0, 字段数量):                    # 按序号遍历每一列
        字段名 = 字典_添加记录_坐标_对象[(0,i)].get()  # 字段名都在第一行
        字段值 = 字典_添加记录_坐标_对象[(1,i)].get()  # 获取用户设置的值
        if 字段值 != '':                            # 如果用户设置的值不是空的
            LT.append((字段名,字段值))              # 加入到插入信息列表
    LT_len = len(LT)                                # 计算插入信息列表长度
    if LT_len != 0:                                 # 不为0说明有插入信息
        SK = ''                                     # 字段名组成字符串,多个字段名用,分割
        SV = ''                                     # 字段值组成字符串,多个字段值用,分割
        for i in range(0, LT_len):                  # 按序号遍历每个插入信息列表
            字段名,字段值 = LT[i]                   # 提取字段名和字段值
            if i == 0:                              # 第一个写法特殊一些
                SK += 字段名                        # 直接加字段名
                SV += '"' + 字段值 + '"'            # 直接加字段值,字段值用引号引起,数据库会根据字段类型自动适应改变类型的
            else:                                   # 后面开始的字段都要加逗号
                SK += ',' + 字段名                  # 先加个逗号和上一个字段名分隔,再加字段名
                SV += ',"' + 字段值 + '"'           # 先加个逗号和上一个字段值分隔,再加字段值,字段值用引号引起
        SQL_CMD = f'INSERT INTO {数据表名} ({SK}) VALUES ({SV})'    # 拼成INSERT SQL语句
        R = DEF_SQL_执行(SQL_CMD)
        if R[0] == 0:
            受影响行数 = R[1]
            INFO = f'数据库 {DB_NAME.get()} 执行SQL语句 {SQL_CMD} 成功,受影响行数:{受影响行数}'
            TEXT_数据库变动日志.insert(0.0, INFO+'\n')
            Log.info(INFO)
            ## 成功后,更新显示表格
            UPDATE_SELECT()
            窗口对象.withdraw()                     # 关闭新窗口
        else:
            ERROR = SQL_CMD + '\n' + R[1]
            print(ERROR)
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        ERROR = '请填入数据'
        print(ERROR)
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)

def 新加记录窗口_取消(窗口对象):
    print("取消")
    窗口对象.withdraw()

def DEF_弹出新加记录窗口(字段名列表):
    新窗口 = Toplevel()
    新窗口.title('添加新记录')

    ## 新窗口布局
    显编框 = Frame(新窗口)
    按钮框 = Frame(新窗口)
    显编框.grid(row=0,column=0,sticky='NW')
    按钮框.grid(row=1,column=0)
    
    ## 宽高参数
    行数 = 2
    列数 = len(字段名列表)
    
    ## 创建画布
    画布 = 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')
    
    ## 动态设置画布窗口宽高:根据屏幕分辨率参数设置限宽限高
    画布限宽 = 屏幕宽 -500         # 比屏幕宽小一点
    #print("画布限宽", 画布限宽)
    if 画布限宽 < 600:
        画布限宽 = 600             # 保障最小宽度
    画布限高 = 屏幕高 -100         # 比屏幕高小一点
    #print("画布限高", 画布限高)
    if 画布限高 < 250:
        画布限高 = 250             # 保障最小高度
    
    ## 设置画布参数
    总行数 = 行数 + 1
    
    ## 画布可滚动显示的最大宽和高(要刚好能放下画布里的Frame里的全部控件)
    画布滚动最右边 = 144*列数      # 140*列数 + 列数*4
    画布滚动最下边 = 21*总行数     # 20*行数 + 行数*1
    
    ## 动态设置显示画布固定显示宽和高(要和主显示框的大小匹配)
    if 画布限宽 > 画布滚动最右边:
        画布['width'] = 画布滚动最右边
    else:
        画布['width'] = 画布限宽 - 30
    
    if 画布限高 > 画布滚动最下边:
        画布['height'] = 画布滚动最下边
    else:
        画布['height'] = 画布限高
    
    画布['scrollregion'] = (0,0,画布滚动最右边,画布滚动最下边)   # 一个元组 tuple (w, n, e, s) ,定义了画布可滚动的最大区域,w 为左边,n 为头部,e 为右边,s 为底部
    
    # 竖滚动条
    Scrollbar_画布_竖 = Scrollbar(显编框, command=画布.yview)
    Scrollbar_画布_竖.grid(row=0,column=1,sticky=S+W+E+N)
    
    # 横滚动条
    Scrollbar_画布_横 = Scrollbar(显编框, command=画布.xview, orient=HORIZONTAL)
    Scrollbar_画布_横.grid(row=1,column=0,sticky=S+W+E+N)

    画布.config(xscrollcommand=Scrollbar_画布_横.set, yscrollcommand=Scrollbar_画布_竖.set) # 自动设置滚动幅度
    画布.create_window((0,0), window=画布Frame框, anchor='nw')

    ## 在 画布里的Frame里创建控件
    字典_添加记录_坐标_对象.clear()
    字典_添加记录_坐标_初值.clear()
    行 = 0                                                                   # 第1行是字段行,序号为0
    for 列 in range(0, 列数):
        初始值 = str(字段名列表[列])
        字典_添加记录_坐标_初值[(行,列)] = 初始值
        字典_添加记录_坐标_对象[(行,列)] = Entry(字段框)
        字典_添加记录_坐标_对象[(行,列)].insert(0, 初始值)
        字典_添加记录_坐标_对象[(行,列)].grid(row=行,column=列,sticky='W')
        字典_添加记录_坐标_对象[(行,列)]['state'] = 'readonly'               # 设置为只读,用户不能修改
    行 = 1                                                                   # 第2行是数据行,序号为1
    for 列 in range(0, 列数):
        字典_添加记录_坐标_初值[(行,列)] = ''
        字典_添加记录_坐标_对象[(行,列)] = Entry(数据框)
        字典_添加记录_坐标_对象[(行,列)].grid(row=行,column=列,sticky='W')
    
    ## 按钮框
    确定按钮 = Button(按钮框, text='确定', command=lambda 窗口对象=新窗口:新加记录窗口_确定(窗口对象))
    确定按钮.grid(row=1,column=0)
    取消按钮 = Button(按钮框, text='取消', command=lambda 窗口对象=新窗口:新加记录窗口_取消(窗口对象))
    取消按钮.grid(row=1,column=1)

    显示坐标 = f'+{屏幕宽//2-(画布限宽//2)}+{屏幕高//2}'
    新窗口.geometry(显示坐标)







####################
## TKinter 主窗口 ##
####################
top = Tk()                                  # 初始化Tk()
top.title('MySQL 图形化数据库管理工具 V1.4')     # 设置标题
窗口宽 = 900
窗口高 = 800
# 获取屏幕尺寸以计算布局参数,使窗口居屏幕中央
屏幕宽 = top.winfo_screenwidth()
屏幕高 = top.winfo_screenheight()
主窗口显示位置 = '%dx%d+%d+%d' % (窗口宽, 窗口高, (屏幕宽-窗口宽)/2, (屏幕高-窗口高)/2)
top.geometry(主窗口显示位置)
top.resizable(width=True, height=True)      # 设置窗口是否可变长、宽(True:可变,False:不可变)



################################
## TKinter 实时更新的全局变量 ##
################################
DB_NAME = StringVar()          # 当前操作的数据库名
DB_TABLE_NAME = StringVar()    # 当前操作的数据库数据表名
SV_最后查询语句 = StringVar()  # 记录上一次的查询语句,用于在修改后刷新显示编辑框内容
SV_查询字段列表 = StringVar()  # 查询语句查询结果的字段信息
字段框_定位列 = IntVar()
数据框_定位行 = IntVar()
数据框_定位列 = IntVar()
新建数据表名 = StringVar()
新表数据库引擎 = StringVar()
新表字符集 = StringVar()

SV_修改表名窗口错误提示 = StringVar()

## 分页
分页行数 = IntVar()            # 设置要读取数据的行数
分页行数.set(10)               # 初始设置以10条分页
IV_已显示记录数 = IntVar()     ### 用于修改数据库后,再次显示在修改位置
IV_上次分页行数 = IntVar()     ### 用于修改数据库后,再次显示在修改位置
IV_记录总行数 = IntVar()       # 记录总行数,通过 游标对象.rowcount 获得

## 选择本地SQL脚本文件
DB_SQL_SCRIPT_FILE = StringVar()    # 选择本地SQL脚本文件

## 光标位置记录(当前就是看看,没有实际用途)
IV_光标X轴 = IntVar()
IV_光标Y轴 = IntVar()

## 设置单元格宽度自动适应时的最大宽度
IV_单元格限宽 = IntVar()               # 自动设置单元格宽度时的最大宽度(字符数)
IV_单元格限宽.set(20)                  # 设置默认最大20字符宽度



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

## 事件函数:右键弹出菜单
def 弹出_数据库表_右键菜单(event):
    光标Y轴值 = event.y
    print("光标Y轴值", 光标Y轴值)
    光标最近项 = Listbox_数据表列表.nearest(光标Y轴值)
    if 光标最近项 != -1:
        #根据光标Y轴位置自动选择
        print("光标最近项", 光标最近项)
        选项_xoffset, 选项_yoffset, 选项_width, 选项_height = Listbox_数据表列表.bbox(光标最近项)
        if 选项_yoffset <= 光标Y轴值 <= 选项_yoffset + 选项_height:        # 光标落在最近项范围内
            Listbox_数据表列表.selection_set(光标最近项)                   # 自动选择光标最近项
            当前选择值 = Listbox_数据表列表.get(光标最近项)
            print("当前选择值", 当前选择值)
            if 当前选择值 != '<空>':
                DB_TABLE_NAME.set(当前选择值)
                数据库表_右键菜单_表名处.post(event.x_root, event.y_root)         # 光标位置显示菜单
                Listbox_数据表列表.selection_clear(光标最近项)
            else:
                数据库表_右键菜单_空白处.post(event.x_root, event.y_root)  # 光标在<空>处显示 数据库表_右键菜单_空白处
        else:                                                              # 光标落在最近项范围外
            print("光标不在选项上,打开空白处用的右键菜单")
            当前选择 = Listbox_数据表列表.curselection()                   # 列表数据定位 (序号数,)
            if 当前选择 != ():
                Listbox_数据表列表.selection_clear(当前选择)
            数据库表_右键菜单_空白处.post(event.x_root, event.y_root)      # 光标位置显示空白处菜单
    else:
        print("无选择项")

## 菜单按钮函数:打开数据库表
def OPEN_TABLE():
    数据库表名 = DB_TABLE_NAME.get()
    ## 打开数据表(查询表内容)
    SQL_CMD = f'SELECT * FROM {数据库表名}'
    DEF_SQL_查询和显示(SQL_CMD)

## 菜单按钮函数:新建数据库表
def ADD_TABLE():
    print("新建表")
    DEF_弹出创建数据表窗口()

## 菜单按钮函数:删除数据库表
def DEL_TABLE():
    print("删除表")
    数据库表名 = DB_TABLE_NAME.get()
    用户决定 = tkinter.messagebox.askquestion(title='请三思...', message='是否确定删除数据表: '+数据库表名)  # 返回值为:yes/no
    if 用户决定 == 'yes':
        print("确定删除表")
        SQL_CMD = f'DROP TABLE {数据库表名}'          # 删除表的SQL语句
        R = DEF_SQL_执行(SQL_CMD)
        if R[0] == 0:
            print("删除数据表成功")
            DEF_查询数据库表()                # 重新查询数据库表
            FRAME_CLEAR(LabelFrame_显编框)    # 清空框内控件
            DB_TABLE_NAME.set('')             # 清空原数据表名记录
        else:
            print("删除数据表失败")
            ERROR = SQL_CMD + '\n' + R[1]
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        print("取消删除表")

## 菜单按钮函数:查看数据库表属性(简略)
def SHOW_TABLE_INFO_7():
    print("查看数据库表属性(简略)")
    数据库表名 = DB_TABLE_NAME.get()
    SQL_CMD = f'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, COLUMN_TYPE, COLUMN_KEY FROM information_schema.columns WHERE TABLE_NAME="{数据库表名}"'
    DEF_SQL_查询和显示(SQL_CMD)

## 菜单按钮函数:查看数据库表属性(详细)
def SHOW_TABLE_INFO():
    print("查看数据库表属性(详细)")
    数据库表名 = DB_TABLE_NAME.get()
    SQL_CMD = f'SELECT * FROM information_schema.columns WHERE TABLE_NAME="{数据库表名}"'
    DEF_SQL_查询和显示(SQL_CMD)

## 菜单按钮函数:重命名据库表
def RENAME_TABLE():
    print("修改表名")
    DEF_弹出修改表名窗口()

## 函数:导出CSV文件
def CSV导出一个表(导出文件名, 导出数据库名, 导出数据表名):
    try:
        F = open(导出文件名, 'a', newline='')    ## newline='' 防止出现每行多一行空行
    except Exception as e:
        ERROR = f'导出数据库"{导出数据库名}"中的数据表"{导出数据表名}"失败\n错误信息:{e}\n请检查文件名或写入权限'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        F_CSV = csv.writer(F)
        ## 查询数据库表提取字段名和数据记录
        SQL_CMD = f'SELECT * FROM {导出数据表名}'
        R = DEF_SQL_查询和返回_数据列表_字段列表(SQL_CMD)
        if R[0] == 0:
            数据记录 = R[1]
            字段信息 = R[2]
            #print(字段信息)
            F_CSV.writerow(字段信息)
            for 记录 in 数据记录:
                #print(记录)
                F_CSV.writerow(记录)
            F.close()
            INFO = f'导出数据库"{导出数据库名}"中的数据表"{导出数据表名}"成功\n导出文件为"{导出文件名}"'
            tkinter.messagebox.showinfo(title='INFO', message=INFO)
        else:
            F.close()
            ERROR = R[1]
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)

## 菜单按钮函数:导出选中的数据库表为CSV格式文件
def 数据表导出CSV():
    print("数据表导出CSV")
    数据库名 = DB_NAME.get()
    数据表名 = DB_TABLE_NAME.get()
    默认导出文件名 = 数据库名 +'_'+ 数据表名 +' ['+ time.strftime('%Y%m%d_%H%M%S') + '].csv'
    导出文件名 = tkinter.simpledialog.askstring(title='导出文件名', prompt='请输入导出文件名:', initialvalue=默认导出文件名)
    print(导出文件名)                                    # 确定为输入内容,取消为None
    if 导出文件名 == None:
        print("取消导出")
    else:
        ## 检查文件名是否可用
        if os.path.exists(导出文件名):	                 # 判断 目录、文件 是否存在
            数据表导出CSV()                              # 文件名已经被使用,循环操作,直到用户输入不重复的文件名或取消
        else:
            CSV导出一个表(导出文件名, 数据库名, 数据表名)

## 菜单按钮函数:导出数据库中每个表为CSV格式文件
def 数据库导出CSV():
    print("数据库导出CSV")
    用户选择结果 = tkinter.messagebox.askyesno(title='数据库导出(csv)', message='导出数据库内全部数据表\n一个CSV只能存储一张表,是否分成多个文件存储')    # 返回值为:True或者False
    print(用户选择结果)
    if 用户选择结果 == True:
        数据库名 = DB_NAME.get()
        数据表列表 = eval(STR_数据表列表内容.get())   # "('表名1', '表名2')" 字符串转成Python数据类型
        for 数据表名 in 数据表列表:
            导出文件名 = 数据库名 +'_'+ 数据表名 +' ['+ time.strftime('%Y%m%d_%H%M%S') + '].csv'
            CSV导出一个表(导出文件名, 数据库名, 数据表名)

def DEF_按钮_显编框数据导出为CSV文件():
    if LabelFrame_显编框.winfo_children() == []:   # 当显示编辑框内组件被销毁后
        ERROR = '无法导出数据:显编框内无数据'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        默认导出文件名 = '导出部分数据 ['+ time.strftime('%Y%m%d_%H%M%S') + '].csv'
        导出文件名 = tkinter.simpledialog.askstring(title='导出显编框内数据(csv)', prompt='所见即所得\n请输入导出文件名:', initialvalue=默认导出文件名)
        print(导出文件名)                                                    # 确定为输入内容,取消为None
        if 导出文件名 == None:
            print("取消导出")
        else:
            ## 检查文件名是否可用
            if os.path.exists(导出文件名):           # 判断 目录、文件 是否存在
                DEF_按钮_显编框数据导出为CSV文件()   # 文件名已经被使用,循环操作,直到用户输入不重复的文件名或取消
            else:
                try:
                    F = open(导出文件名, 'a', newline='')    ## newline='' 防止出现每行多一行空行
                except Exception as e:
                    ERROR = f'导出数据"{导出文件名}"失败\n错误信息:{e}\n请检查文件名或写入权限'
                    tkinter.messagebox.showerror(title='ERROR', message=ERROR)
                else:
                    F_CSV = csv.writer(F)
                    ## 使用显示编辑框内实时数据,用户可以修改而不改动数据库,直接导出数据,所见即所得
                    列表_字段信息 = []
                    for K in 字典_查询字段_坐标_对象:
                        列表_字段信息.append(字典_查询字段_坐标_对象[K].get())
                    F_CSV.writerow(列表_字段信息)
                    列数 = len(列表_字段信息)
                    N = 0
                    列表_数据信息 = []
                    for K in 字典_查询结果_坐标_对象:
                        N += 1
                        列表_数据信息.append(字典_查询结果_坐标_对象[K].get())
                        if N%列数==0:
                            F_CSV.writerow(列表_数据信息)
                            列表_数据信息 = []
                            N = 0
                    F.close()
                    INFO = f'导出数据"{导出文件名}"成功'
                    tkinter.messagebox.showinfo(title='INFO', message=INFO)

def DEF_按钮_显编框数据导出为Excel文件():
    if LabelFrame_显编框.winfo_children() == []:   # 当显示编辑框内组件被销毁后
        ERROR = '无法导出数据:显编框内无数据'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        默认导出文件名 = '导出部分数据 ['+ time.strftime('%Y%m%d_%H%M%S') + '].xlsx'
        导出文件名 = tkinter.simpledialog.askstring(title='导出显编框内数据(xlsx)', prompt='所见即所得\n请输入导出文件名:', initialvalue=默认导出文件名)
        # 确定为输入内容,取消为None
        if 导出文件名 == None:
            print("取消导出")
        else:
            ## 检查文件名是否可用
            if os.path.exists(导出文件名):               # 判断 目录、文件 是否存在
                DEF_按钮_显编框数据导出为Excel文件()     # 文件名已经被使用,循环操作,直到用户输入不重复的文件名或取消
            else:
                try:
                    from openpyxl import Workbook
                except Exception as e:
                    ERROR = f'导出数据"{导出文件名}"失败\n错误信息:{e}\n请检查写入权限或openpyxl模块的安装和加载'
                    tkinter.messagebox.showerror(title='ERROR', message=ERROR)
                else:
                    EXCEL文件 = Workbook()
                    工作表 = EXCEL文件.active            # 获得激活的worksheet,默认有一张名为Sheet的工作表
                    ## 使用显示编辑框内实时数据,用户可以修改而不改动数据库,直接导出数据,所见即所得
                    列表_字段信息 = []
                    for K in 字典_查询字段_坐标_对象:
                        列表_字段信息.append(字典_查询字段_坐标_对象[K].get())
                    工作表.append(列表_字段信息)
                    列数 = len(列表_字段信息)
                    N = 0
                    列表_数据信息 = []
                    for K in 字典_查询结果_坐标_对象:
                        N += 1
                        列表_数据信息.append(字典_查询结果_坐标_对象[K].get())
                        if N%列数==0:
                            工作表.append(列表_数据信息)
                            列表_数据信息 = []
                            N = 0
                    EXCEL文件.save(导出文件名)
                    INFO = f'导出数据"{导出文件名}"成功'
                    tkinter.messagebox.showinfo(title='INFO', message=INFO)

## 菜单按钮函数:导出选中的数据库表为EXCEL格式文件
def 数据表导出EXCEL():
    print("数据表导出EXCEL")
    数据库名 = DB_NAME.get()
    数据表名 = DB_TABLE_NAME.get()
    默认导出文件名 = 数据库名 +'_'+ 数据表名 +' ['+ time.strftime('%Y%m%d_%H%M%S') + '].xlsx'
    导出文件名 = tkinter.simpledialog.askstring(title='导出数据表(xlsx)', prompt='导出数据表\n最多导出1048575行数据\n请输入导出文件名:', initialvalue=默认导出文件名)
    print(导出文件名)                                                    # 确定为输入内容,取消为None
    if 导出文件名 == None:
        print("取消导出")
    else:
        ## 检查文件名是否可用
        if os.path.exists(导出文件名):   # 判断 目录、文件 是否存在
            数据表导出EXCEL()            # 文件名已经被使用,循环操作,直到用户输入不重复的文件名或取消
        else:
            try:
                from openpyxl import Workbook
            except Exception as e:
                ERROR = f'导出数据库"{数据库名}"中的数据表"{数据表名}"失败\n错误信息:{e}\n请检查写入权限或openpyxl模块的安装和加载'
                tkinter.messagebox.showerror(title='ERROR', message=ERROR)
            else:
                ## 查询数据库表提取字段名和数据记录
                SQL_CMD = f'SELECT * FROM {数据表名}'
                R = DEF_SQL_查询和返回_数据列表_字段列表(SQL_CMD)
                if R[0] == 0:
                    数据记录 = R[1]
                    字段信息 = R[2]
                    EXCEL文件 = Workbook()
                    工作表 = EXCEL文件.active      # 获得激活的worksheet,默认有一张名为Sheet的工作表
                    工作表.title = 数据表名        # 重命名当前工作表
                    工作表.append(字段信息)        # 字段行放第一行作为标题
                    for i in 数据记录:
                        工作表.append(i)           # 写入数据行
                    EXCEL文件.save(导出文件名)     # 保存
                    INFO = f'导出数据库"{数据库名}"中的数据表"{数据表名}"成功\n导出文件为"{导出文件名}"'
                    tkinter.messagebox.showinfo(title='INFO', message=INFO)
                else:
                    ERROR = R[1]
                    tkinter.messagebox.showerror(title='ERROR', message=ERROR)

## 菜单按钮函数:导出数据库中每个表为一个工作表的Excel格式文件
def 数据库导出EXCEL():
    print("数据库导出EXCEL")
    数据库名 = DB_NAME.get()
    默认导出文件名 = 数据库名 +' ['+ time.strftime('%Y%m%d_%H%M%S') + '].xlsx'
    导出文件名 = tkinter.simpledialog.askstring(title='导出数据库内全部数据表(xlsx)', prompt='导出数据库内全部表\n最多导出1048575行数据\n请输入导出文件名:', initialvalue=默认导出文件名)
    ## 确定为输入内容,取消为None
    if 导出文件名 == None:
        print("取消导出")
    else:
        ## 检查文件名是否可用
        if os.path.exists(导出文件名):	                 # 判断 目录、文件 是否存在
            数据库导出EXCEL()                            # 文件名已经被使用,循环操作,直到用户输入不重复的文件名或取消
        else:
            try:
                from openpyxl import Workbook
            except Exception as e:
                ERROR = f'导出数据库"{数据库名}"失败\n错误信息:{e}\n请检查写入权限或openpyxl模块的安装和加载'
                tkinter.messagebox.showerror(title='ERROR', message=ERROR)
            else:
                数据表列表 = eval(STR_数据表列表内容.get())   # "('表名1', '表名2')" 字符串转成Python数据类型
                EXCEL文件 = Workbook()                        # 创建Excel文件
                for 数据表名 in 数据表列表:
                    ## 查询数据库表提取字段名和数据记录
                    SQL_CMD = f'SELECT * FROM {数据表名}'
                    R = DEF_SQL_查询和返回_数据列表_字段列表(SQL_CMD)
                    if R[0] == 0:
                        数据记录 = R[1]
                        字段信息 = R[2]
                        工作表 = EXCEL文件.create_sheet(数据表名, 0)   # 创建工作表并插入到最前的位置
                        工作表.append(字段信息)                        # 字段行放第一行作为标题
                        for i in 数据记录:
                            工作表.append(i)                           # 写入数据行
                        INFO = f'导出数据库"{数据库名}"中数据表"{数据表名}"成功\n导出文件为"{导出文件名}"'
                        tkinter.messagebox.showinfo(title='INFO', message=INFO)
                    else:
                        ERROR = R[1]
                        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
                EXCEL文件.save(导出文件名)     # 保存

## 创建数据表列表框-表名处右键菜单
数据库表_右键菜单_表名处 = Menu()
数据库表_右键菜单_表名处.add_command(label='打开表', command=OPEN_TABLE)
数据库表_右键菜单_表名处.add_command(label='删除表', command=DEL_TABLE)
数据库表_右键菜单_表名处.add_command(label='重命名', command=RENAME_TABLE)
数据库表_右键菜单_表名处.add_command(label='表属性(简略)', command=SHOW_TABLE_INFO_7)
数据库表_右键菜单_表名处.add_command(label='表属性(详细)', command=SHOW_TABLE_INFO)
数据库表_右键菜单_表名处.add_command(label='导出表(CSV)', command=数据表导出CSV)
数据库表_右键菜单_表名处.add_command(label='导出表(Excel)最大1048575行记录', command=数据表导出EXCEL)

## 创建数据表列表框-空白处右键菜单
数据库表_右键菜单_空白处 = Menu()
数据库表_右键菜单_空白处.add_command(label='创建表', command=ADD_TABLE)
数据库表_右键菜单_空白处.add_command(label='导出库(CSV)', command=数据库导出CSV)
数据库表_右键菜单_空白处.add_command(label='导出库(Excel)每个表最大1048575行记录', command=数据库导出EXCEL)



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

## 事件函数:右键菜单
def DEF_弹出_字段框_右键菜单(event):
    # 取值
    选中控件 = event.widget
    行 = 0                               # 字段名只有1行,恒等于0
    列 = 选中控件.grid_info()['column']
    # 赋值
    字段框_定位列.set(列)
    ## 选择的控件变红
    #选中控件['bg'] = 'red'
    ## 弹出菜单
    字段框_右键菜单.post(event.x_root, event.y_root)   # 光标位置显示菜单

## 菜单按钮函数:添加列(添加字段)
def ADD_COL():
    print("添加列(添加字段)")
    DEF_弹出新加字段窗口()

## 菜单按钮函数:删除列(删除字段)
## ALTER TABLE 表名 DROP COLUMN 列名
def DEL_COL():
    数据库表名 = DB_TABLE_NAME.get()
    字段索引 = 字段框_定位列.get()
    字段名 = 字典_查询字段_坐标_初值[(0,字段索引)]
    print("删除列(删除字段)", 字段名)
    用户决定 = tkinter.messagebox.askquestion(title='请三思...', message='是否确定删除字段: '+字段名)  # 返回值为:yes/no
    if 用户决定 == 'yes':
        print("确定删除字段", 字段名)
        SQL_CMD = f'ALTER TABLE {数据库表名} DROP COLUMN {字段名}'          # 删除字段的SQL语句
        R = DEF_SQL_执行(SQL_CMD)
        if R[0] == 0:
            print("删除字段成功")
            UPDATE_SELECT()               # 重新查询数据库表,更新显编框内容
        else:
            print("删除字段失败")
            ERROR = SQL_CMD + '\n' + R[1]
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        print("取消删除字段", 字段名)

## 菜单按钮函数:编辑列(修改字段)
def EDIT_COL():
    print("编辑列(修改字段)")
    DEF_弹出修改字段窗口()

## 菜单按钮函数:新增行(新增记录)
def ADD_DATA():
    print("添加数据记录")
    DEF_新增记录()          # 调用新增记录函数

## 创建字段框右键菜单
字段框_右键菜单 = Menu()
字段框_右键菜单.add_command(label='添加列(添加字段)', command=ADD_COL)
字段框_右键菜单.add_command(label='删除列(删除字段)', command=DEL_COL)
字段框_右键菜单.add_command(label='编辑列(修改字段)', command=EDIT_COL)
字段框_右键菜单.add_separator()                                            # 分割线
字段框_右键菜单.add_command(label='添加数据记录', command=ADD_DATA)





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

## 事件函数:离开控件
def 离开控件(event):
    # 取值
    离开控件 = event.widget
    离开行 = 离开控件.grid_info()['row']
    离开列 = 离开控件.grid_info()['column']
    ## 判断内容是否有变动
    #print("刚刚离开(行,列)", (离开行,离开列))
    if 数据框_定位行.get() == 离开行 and 数据框_定位列.get() == 离开列:   # 应该是多余的判断,先留着DEBUG
        新值 = 字典_查询结果_坐标_对象[(离开行,离开列)].get()
        #print("刚刚离开的新值", 新值)
        旧值 = 字典_查询结果_坐标_初值[(离开行,离开列)]
        #print("刚刚离开的旧值", 旧值)
        if 新值 == 旧值:
            print("离开控件:无变化")
            离开控件['bg'] = '#FFFFFF'                            # 无变化还原白底
            #解禁分页按钮
            按钮_显编框下一页['state'] = 'normal'
        else:
            print("离开控件:有变化")
            离开控件['bg'] = '#7FFF00'                            # 有变化改成草绿
            按钮_确认修改数据库.grid()                            # 显示修改数据库的按钮
            # 禁止分页按钮
            按钮_显编框下一页['state'] = 'disabled'               # 禁止下一页按钮
    else:
        print("从其他地方离开,忽略")
    字典_查询结果_坐标_对象[(离开行,离开列)].unbind('<Leave>')    # 离开后解除控件的离开事件
    文本框_命令行.focus_set()                                     # 焦点移到命令文本输入框

## 事件函数:左键单击
def 左键单击(event):
    # 取值
    选中控件 = event.widget
    行 = 选中控件.grid_info()['row']
    列 = 选中控件.grid_info()['column']
    #选中控件['bg'] = '#7FFF00'
    # 赋值
    数据框_定位行.set(行)
    数据框_定位列.set(列)
    字典_查询结果_坐标_对象[(行,列)].bind('<Leave>', 离开控件)  # 单击是进入编辑,给这个控件加个离开事件

## 事件函数:右键菜单
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 ADD_ROW():
    DEF_新增记录()

## 菜单按钮函数:删除整行(提取控件行号信息)
def DEL_ROW():
    控件行号 = 数据框_定位行.get()
    DEF_删除记录(控件行号)
    ## 删除成功后的行列号和当前行列号有差别,立刻设置为无效行列号,防止后面误删
    数据框_定位行.set(-1)
    数据框_定位列.set(-1)

## 菜单按钮函数:删除整行(根据控件行号删除)
def DEF_删除记录(控件行号):
    数据表名 = DB_TABLE_NAME.get()
    字段名列表 = DB_INFO['字段名列表']
    ## 找主键名:查数据库表得到主键信息 L_PK_NAME
    R = 主键名列表(数据表名)
    if R[0] == 0:
        L_PK_NAME = R[1]
        主键数量 = len(L_PK_NAME)
        if 主键数量 == 0:
            ERROR = f'数据表“{数据表名}”没有主键,为防止误删除,本次不执行删除。'
            tkinter.messagebox.showerror(title='ERROR', message=ERROR)
        else:
            ## 一个或多个主键的情况
            列表_主键信息 = []                                           # 查询显示结果中的主键字段名信息列表
            for 主键名 in L_PK_NAME:
                for 列号 in range(0, len(字段名列表)):                   # 按下标遍历查询显示结果中每个字段名
                    if 字段名列表[列号] == 主键名:                       # 如果含有主键字段
                        主键信息 = (列号,主键名)                         # 记录在查询显示结果中的下标和对应主键字段名
                        列表_主键信息.append(主键信息)                   # 添加到主键字段名信息列表,[(列号1,主键名1), (列号2,主键名2)]
            if 列表_主键信息 == []:
                ERROR = f'主键字段{L_PK_NAME}未包含在当前查询结果中,当前查询字段列表{字段名列表},为防止误删除,本次不执行删除。'
                tkinter.messagebox.showerror(title='ERROR', message=ERROR)
            elif len(列表_主键信息) != 主键数量:
                ERROR = f'主键字段{L_PK_NAME}未全部包含在当前查询结果中,当前查询字段列表{字段名列表},为防止误删除,本次不执行删除。'
                tkinter.messagebox.showerror(title='ERROR', message=ERROR)
            else:
                ## 主键大于等于1且都在显编框内
                SQL_CMD = f'DELETE FROM {数据表名} WHERE '
                for i in range(0, len(列表_主键信息)):
                    (列号,主键名) = 列表_主键信息[i]                         # 获得主键名
                    控件列号 = 列号
                    主键值 = 字典_查询结果_坐标_初值[(控件行号,控件列号)]    # 获得主键值
                    print("主键名", 主键名, "主键值", 主键值)
                    if i == 0:
                        SQL_CMD += f'{主键名}="{主键值}"'
                    else:
                        SQL_CMD += f' AND {主键名}="{主键值}"'
                print("SQL_CMD", SQL_CMD)
                RR = DEF_SQL_执行(SQL_CMD)
                if RR[0] == 0:
                    ###UPDATE_SELECT()                  # 刷新显编框:从头显示
                    UPDATE_SELECT_LINIT()               # 刷新显编框:定位到修改处
                    受影响行数 = RR[1]
                    INFO = f'数据库 {DB_NAME.get()} 执行SQL语句 {SQL_CMD} 成功,受影响行数:{受影响行数}'
                    TEXT_数据库变动日志.insert(0.0, INFO+'\n')
                    Log.info(INFO)
                else:
                    ERROR = f'数据库 {DB_NAME.get()} 执行SQL语句 {SQL_CMD} 失败 {RR[1]}'
                    tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        ERROR = f'查询数据表"{数据表名}"主键信息失败:{R[1]}'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)

## 创建数据框右键菜单
数据框_右键菜单 = Menu()
数据框_右键菜单.add_command(label='添加新行', command=ADD_ROW)
数据框_右键菜单.add_command(label='删除整行', command=DEL_ROW)
数据框_右键菜单.add_separator()                                                # 分隔线
数据框_右键菜单.add_command(label='大文本编辑', command=DEF_弹出大文本窗口)







################################
## TKinter 主窗口布局 - TOP框 ##
################################
顶框 = Frame(top)
左侧框 = Frame(top, bg='#00CED1')
分隔左右框 = Frame(top, width=20)
右侧框 = Frame(top)
日志框 = LabelFrame(top, text='数据库改动记录(倒序)')

顶框.grid(row=0,column=0,sticky='NW', columnspan=3)   #0-012
日志框.grid(row=1,column=0,sticky='NW', columnspan=3) #1-012
左侧框.grid(row=2,column=0,sticky='NW')               #2-0
分隔左右框.grid(row=2,column=1,sticky='NW')           #2-1
右侧框.grid(row=2,column=2,sticky='NW')               #2-2



#######################################
## TKinter 主窗口布局 - TOP框 - 顶框 ##
#######################################

数据库连接操作框 = Frame(顶框)
数据库连接操作框.grid(row=0,column=0,sticky='NW')     #0-0

##########################################################
## TKinter 主窗口布局 - TOP框 - 顶框 - 数据库连接操作框 ##
##########################################################

SV_提示信息 = StringVar()
SV_登录地址 = StringVar()
IV_服务端口 = IntVar()
SV_登录库名 = StringVar()
SV_字符集 = StringVar()
SV_登录帐号 = StringVar()
SV_登录密码 = StringVar()

# 设置默认值,方便测试
SV_登录地址.set('192.168.0.1')
IV_服务端口.set(3306)
SV_登录库名.set('PY')
SV_字符集.set('UTF8')
SV_登录帐号.set('root')
SV_登录密码.set('pwd@123')

def DEF_按钮_打开数据库():
    SV_提示信息.set('正在连接...')
    登录地址 = SV_登录地址.get()
    服务端口 = IV_服务端口.get()
    登录帐号 = SV_登录帐号.get()
    登录密码 = SV_登录密码.get()
    登录库名 = SV_登录库名.get()
    字符集 = SV_字符集.get()
    R = DEV_MySQL_OPEN(登录地址, 登录帐号, 登录密码, 服务端口, 登录库名, 字符集)
    #print("R", R)
    if R[0] == 0:
        DB_NAME.set(登录库名)                                       # 设置数据库名(查看和记录日志用)
        SV_登录密码.set('')                                         # 清除密码信息
        Frame_数据表列表显示框.grid(row=1,column=0,sticky='NW')     # 显示Frame_数据表列表显示框
        INFO = '连接成功'
        SV_提示信息.set(INFO)
        Label_提示信息['fg'] = 'green'
    else:
        ERROR = '连接失败:\n' + R[1]
        SV_提示信息.set(ERROR)
        SV_登录密码.set('')                                         # 清除密码信息
        Label_提示信息['fg'] = 'red'

def DEF_按钮_关闭数据库():
    SV_提示信息.set('正在关闭数据库')
    数据库连接对象 = DB_INFO['数据库连接']
    if 数据库连接对象 == '':
        ERROR = '数据库未打开或已关闭'
        print(ERROR)
        SV_提示信息.set(ERROR)
        Label_提示信息['fg'] = 'red'
    else:
        数据库连接对象.close()
        Frame_数据表列表显示框.grid_forget()
        FRAME_CLEAR(LabelFrame_显编框)         # 清空显示编辑框内控件
        ## 清空全局变量
        DB_INFO['数据库连接'] = ''
        字典_查询字段_坐标_对象 = {}
        字典_查询字段_坐标_初值 = {}
        字典_查询结果_坐标_对象 = {}
        字典_查询结果_坐标_初值 = {}
        字典_添加记录_坐标_对象 = {}
        字典_添加记录_坐标_初值 = {}
        字典_创建表_字段信息 = {}
        字典_新加字段信息 = {}
        字典_对象存储 = {}
        SV_提示信息.set('数据库已经关闭')
        Label_提示信息['fg'] = 'red'

## 提示框
数据库登录状态提示框 = Frame(数据库连接操作框)
Label_提示信息 = Label(数据库登录状态提示框, textvariable=SV_提示信息)
Label_提示信息.grid(row=0,column=0,sticky='NW')
数据库登录状态提示框.grid(row=0,column=0,sticky='NW')

## 登录信息框
数据库登录框 = Frame(数据库连接操作框)
Label(数据库登录框, text='[登录地址]').grid(       row=0,column=0,sticky='W')
Entry(数据库登录框, textvariable=SV_登录地址).grid(row=0,column=1,sticky='W')
Label(数据库登录框, text='[服务端口]').grid(       row=0,column=2,sticky='W')
Entry(数据库登录框, textvariable=IV_服务端口).grid(row=0,column=3,sticky='W')
Label(数据库登录框, text='[登录库名]').grid(       row=0,column=4,sticky='W')
Entry(数据库登录框, textvariable=SV_登录库名).grid(row=0,column=5,sticky='W')
Label(数据库登录框, text='[字符集]').grid(         row=0,column=6,sticky='W')
Entry(数据库登录框, textvariable=SV_字符集).grid(  row=0,column=7,sticky='W')
Label(数据库登录框, text='[登录帐号]').grid(                 row=1,column=0,sticky='W')
Entry(数据库登录框, textvariable=SV_登录帐号).grid(          row=1,column=1,sticky='W')
Label(数据库登录框, text='[登录密码]').grid(                 row=1,column=2,sticky='W')
Entry(数据库登录框, textvariable=SV_登录密码, show='*').grid(row=1,column=3,sticky='W')
数据库登录框.grid(row=1,column=0,sticky='NW')

## 登录按钮框
数据库按钮框 = Frame(数据库连接操作框)
数据库按钮框.grid(row=2,column=0,sticky='NW')
Button(数据库按钮框, text='打开数据库', command=DEF_按钮_打开数据库).grid(row=0,column=0,sticky='NW')
Button(数据库按钮框, text='关闭数据库', command=DEF_按钮_关闭数据库).grid(row=0,column=1,sticky='NW')



#########################################
## TKinter 主窗口布局 - TOP框 - 日志框 ##
#########################################

TEXT_数据库变动日志 = Text(日志框, width=120, height=3, wrap='none')    # 显示改动了数据库的操作日志
TEXT_数据库变动日志.grid(row=0, column=0, sticky='NW')

Scrollbar_日志框_竖 = Scrollbar(日志框) 
Scrollbar_日志框_竖['command'] = TEXT_数据库变动日志.yview
Scrollbar_日志框_横 = Scrollbar(日志框) 
Scrollbar_日志框_横['command'] = TEXT_数据库变动日志.xview
Scrollbar_日志框_横['orient'] = HORIZONTAL
Scrollbar_日志框_竖.grid(row=0, column=1, sticky=S+W+E+N)
Scrollbar_日志框_横.grid(row=1, column=0, sticky=S+W+E+N)
TEXT_数据库变动日志.config(xscrollcommand=Scrollbar_日志框_横.set, yscrollcommand=Scrollbar_日志框_竖.set)  # 自动设置滚动条滑动幅度



#########################################
## TKinter 主窗口布局 - TOP框 - 左侧框 ##
#########################################

Frame_数据表列表显示框 = LabelFrame(左侧框, text='数据库内的数据表')
全局变量框 = LabelFrame(左侧框, text='全局变量框,实时更新,请勿修改')
#Frame_数据表列表显示框.grid(row=1,column=0,sticky='NW') #1-0   # 由打开数据库按钮控制显示
全局变量框.grid(row=2,column=0,sticky='NW')             #2-0

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

STR_数据表列表内容 = StringVar()     # 实时更新变量

## Listbox 列表控件
Listbox_数据表列表 = Listbox(Frame_数据表列表显示框, listvariable=STR_数据表列表内容, height=8, width=30)    # height 行数(默认10行)

## Scrollbar 滚动条控件
Scrollbar_数据表列表_横 = Scrollbar(Frame_数据表列表显示框, orient=HORIZONTAL, command=Listbox_数据表列表.xview) # HORIZONTAL 横向
Scrollbar_数据表列表_竖 = Scrollbar(Frame_数据表列表显示框, orient=VERTICAL, command=Listbox_数据表列表.yview)   # VERTICAL 纵向(默认就是)

## 列表控件 绑定事件、设置滚动条
Listbox_数据表列表.config(xscrollcommand=Scrollbar_数据表列表_横.set)
Listbox_数据表列表.config(yscrollcommand=Scrollbar_数据表列表_竖.set)
Listbox_数据表列表.bind('<Double-Button-1>', DEF_双击表名)             # 绑定双击命令
Listbox_数据表列表.bind('<Button-3>', 弹出_数据库表_右键菜单)          # 绑定右键菜单事件

## 控件布局
Listbox_数据表列表.grid(row=0,column=0,sticky='NW')          #00
Scrollbar_数据表列表_横.grid(row=1,column=0,sticky=S+W+E+N)  #10
Scrollbar_数据表列表_竖.grid(row=0,column=1,sticky=S+W+E+N)  #01
Button(Frame_数据表列表显示框, text='新建表', command=ADD_TABLE).grid(row=2,column=0,sticky='W')   # 20

######################################################
## TKinter 主窗口布局 - TOP框 - 左侧框 - 全局变量框 ##
######################################################

数据库信息框 = LabelFrame(全局变量框, text='数据库信息')
Label(数据库信息框, text='[数据库名]').grid(           row=0,column=0,sticky='W')
Entry(数据库信息框, textvariable=DB_NAME).grid(        row=0,column=1,sticky='W')
Label(数据库信息框, text='[数据表名]').grid(           row=1,column=0,sticky='W')
Entry(数据库信息框, textvariable=DB_TABLE_NAME).grid(  row=1,column=1,sticky='W')
Label(数据库信息框, text='[最后查询语句]').grid(       row=2,column=0,sticky='W')
Entry(数据库信息框, textvariable=SV_最后查询语句).grid(row=2,column=1,sticky='W')
Label(数据库信息框, text='[查询字段列表]').grid(       row=3,column=0,sticky='W')
Entry(数据库信息框, textvariable=SV_查询字段列表).grid(row=3,column=1,sticky='W')
Label(数据库信息框, text='[新建数据表名]').grid(       row=4,column=0,sticky='W')
Entry(数据库信息框, textvariable=新建数据表名).grid(   row=4,column=1,sticky='W')
数据库信息框.grid(row=0,column=0,sticky='W', columnspan=2)

显编框_字段框 = LabelFrame(全局变量框, text='显编框.字段框定位')
Label(显编框_字段框, text='[列]').grid(row=0,column=0,sticky='W')
Entry(显编框_字段框, textvariable=字段框_定位列, width=3).grid(row=0,column=1,sticky='W')
显编框_字段框.grid(row=1,column=0,sticky='W')

显编框_数据框 = LabelFrame(全局变量框, text='显编框.数据框定位')
Label(显编框_数据框, text='[行]').grid(row=0,column=0,sticky='W')
Entry(显编框_数据框, textvariable=数据框_定位行, width=3).grid(row=0,column=1,sticky='W')
Label(显编框_数据框, text='[列]').grid(row=0,column=2,sticky='W')
Entry(显编框_数据框, textvariable=数据框_定位列, width=3).grid(row=0,column=3, sticky='W')
显编框_数据框.grid(row=1,column=1,sticky='E')

## 查看光标位置(当前就是看看,没有实际用途)
光标定位框 = LabelFrame(全局变量框, text='光标定位框')
Label(光标定位框, text='[光标X轴]').grid(row=0,column=0,sticky='W')
Entry(光标定位框, textvariable=IV_光标X轴, width=6).grid(row=0, column=1, sticky='W',columnspan=3)
Label(光标定位框, text='[光标Y轴]').grid(row=1,column=0,sticky='W')
Entry(光标定位框, textvariable=IV_光标Y轴, width=6).grid(row=1, column=1, sticky='W',columnspan=3)
光标定位框.grid(row=2,column=0,sticky='W')

显编框_编辑后定位框 = LabelFrame(全局变量框, text='显编框_编辑后定位框')
Label(显编框_编辑后定位框, text='[已显示记录数]').grid(             row=0,column=0,sticky='W')
Entry(显编框_编辑后定位框, textvariable=IV_已显示记录数, width=3).grid(row=0,column=1, sticky='W')
Label(显编框_编辑后定位框, text='[上次分页行数]').grid(             row=1,column=0,sticky='W')
Entry(显编框_编辑后定位框, textvariable=IV_上次分页行数, width=3).grid(row=1,column=1, sticky='W')
显编框_编辑后定位框.grid(row=2,column=1,sticky='W')

显编框_分页控制框 = LabelFrame(全局变量框, text='显编框_分页控制框')
Label(显编框_分页控制框, text='[分页行数]').grid(             row=0,column=0,sticky='W')
Entry(显编框_分页控制框, textvariable=分页行数, width=6).grid(row=0,column=1, sticky='W')
Label(显编框_分页控制框, text='[记录总数]').grid(             row=1,column=0,sticky='W')
Entry(显编框_分页控制框, textvariable=IV_记录总行数, width=6).grid(row=1,column=1, sticky='W')
显编框_分页控制框.grid(row=3,column=0,sticky='W')

显编框_单元格限宽 = LabelFrame(全局变量框, text='显编框_单元格限宽')
Label(显编框_单元格限宽, text='[单元格限宽]').grid(             row=0,column=0,sticky='W')
Entry(显编框_单元格限宽, textvariable=IV_单元格限宽, width=6).grid(row=0,column=1, sticky='W')
显编框_单元格限宽.grid(row=3,column=1,sticky='W')



#########################################
## TKinter 主窗口布局 - TOP框 - 右侧框 ##
#########################################

LabelFrame_显编框 = LabelFrame(右侧框, text='显示/编辑框', bg='#FFD700')
分页按钮框 = Frame(右侧框)
导出数据框 = Frame(右侧框)
修改确认框 = Frame(右侧框)
命令框 = LabelFrame(右侧框, text='SQL语句/SQL脚本')
常用功能框_表内全字段搜索 = LabelFrame(右侧框, text='常用功能框:表内全字段搜索')  # 【V1.4】
快速命令框 = LabelFrame(右侧框, text='自定义命令框')

## 框架的位置布局
LabelFrame_显编框.grid(row=0,column=0,sticky='NW')  #0-0
分页按钮框.grid(row=1,column=0,sticky='NW')         #1-0
导出数据框.grid(row=2,column=0,sticky='NW')         #2-0
修改确认框.grid(row=3,column=0,sticky='NW')         #3-0
常用功能框_表内全字段搜索.grid(row=4,column=0,sticky='NW')         #4-0 【V1.4】
命令框.grid(row=5,column=0,sticky='NW')             #5-0
快速命令框.grid(row=6,column=0,sticky='NW')         #6-0

######################################################
## TKinter 主窗口布局 - TOP框 - 右侧框 - 分页按钮框 ##
######################################################

按钮_显编框起始页 = Button(分页按钮框, text='返回起始页/刷新', command=DEF_按钮_显编框起始页)
按钮_显编框下一页 = Button(分页按钮框, text='下一页', command=DEF_按钮_显编框下一页)
按钮_显编框起始页.grid(row=0,column=0, sticky='NW') # 显示起始页按钮
按钮_显编框下一页.grid(row=0,column=1, sticky='NW') # 显示下一页按钮
Label(分页按钮框, text='            \t').grid(row=0,column=3,sticky='E')                  # 填充空间
Label(分页按钮框, text='[分页显示行数]').grid(row=0,column=4,sticky='E')
Combobox_分页显示行数 = ttk.Combobox(分页按钮框, width=3)
Combobox_分页显示行数['value'] = (5, 10, 20, 50, 100, 200, 300, 500)
Combobox_分页显示行数.current(1)                                                # 默认值中的内容为索引,从0开始
Combobox_分页显示行数.grid(row=0, column=5, sticky='E')
Label(分页按钮框, text='            ').grid(row=0,column=6,sticky='E')                  # 填充空间
Label(分页按钮框, text='总行数:').grid(            row=0,column=7,sticky='E')
Label(分页按钮框, textvariable=IV_记录总行数).grid(row=0,column=8,sticky='E')

def 选择后执行函数(event):
    分页行数.set(Combobox_分页显示行数.get())

Combobox_分页显示行数.bind('<<ComboboxSelected>>', 选择后执行函数)

Button(导出数据框, text='显编框内数据导出(CSV)', command=DEF_按钮_显编框数据导出为CSV文件).grid(row=1,column=0,sticky='E')
Button(导出数据框, text='显编框内数据导出(Excel)', command=DEF_按钮_显编框数据导出为Excel文件).grid(row=1,column=1,sticky='E')

## 不用时禁止
#按钮_显编框起始页['state'] = 'disabled'
按钮_显编框下一页['state'] = 'disabled'
## 需要时启用
#按钮_显编框起始页['state'] = 'normal'
#按钮_显编框下一页['state'] = 'normal'

######################################################
## TKinter 主窗口布局 - TOP框 - 右侧框 - 修改确认框 ##
######################################################

按钮_确认修改数据库 = Button(修改确认框, text='确认修改', bg='#7FFF00', command=DEF_按钮_确认修改数据库)
#进行编辑后再出现
按钮_确认修改数据库.grid(row=0,column=0, sticky='NW')
按钮_确认修改数据库.grid_forget()            # 隐藏
#按钮_确认修改数据库.grid()                  # 显示

##################################################
## TKinter 主窗口布局 - TOP框 - 右侧框 - 命令框 ##
##################################################

## 选择本地SQL脚本文件
def DEF_按钮_选择SQL脚本文本():
    本地SQL脚本文本 = filedialog.askopenfilename()
    DB_SQL_SCRIPT_FILE.set(本地SQL脚本文本)              # 实时更新显示

## 执行本地SQL脚本文件
def DEF_按钮_执行SQL脚本文件():
    脚本文件 = DB_SQL_SCRIPT_FILE.get().strip()
    if 脚本文件 == '':
        ERROR = '没有脚本文件可以执行'
        tkinter.messagebox.showerror(title='错误', message=ERROR)
    else:
        try:
            f = open(脚本文件, 'r')
        except Exception as e:
            ERROR = str(e)
            tkinter.messagebox.showerror(title='错误', message=ERROR)
        else:
            脚本文件内容 = f.read()
            f.close()
            if 脚本文件内容 != '':
                R = DEF_SQL_执行多条SQL语句_事件控制(脚本文件内容)
                if R[0] == 0:
                    UPDATE_SELECT()
                    INFO = f'数据库 {DB_NAME.get()} 执行SQL脚本文件 {脚本文件} 内容 {脚本文件内容} 成功'
                    TEXT_数据库变动日志.insert(0.0, INFO+'\n')
                    Log.info(INFO)
                    tkinter.messagebox.showinfo(title='成功', message=INFO)
                else:
                    ERROR = f'数据库 {DB_NAME.get()} 执行SQL脚本文件 {脚本文件} 内容 {脚本文件内容} 失败 {R[1]}'
                    tkinter.messagebox.showerror(title='失败', message=ERROR)
                FRAME_CLEAR(LabelFrame_显编框)                    # 清空显示编辑框内控件
            else:
                ERROR = 'SQL脚本文件无内容'
                tkinter.messagebox.showerror(title='错误', message=ERROR)

## 执行用户输入的单条SQL语句(使用默认的自动事件控制)
def DEF_按钮_执行单条SQL语句():
    SQL_CMD = 文本框_命令行.get(1.0, END).rstrip('\n')     # 获取编写的SQL语句,去掉后面的回车符号
    单条语句内容 = SQL_CMD.strip()                         # 去掉首尾空格
    if 单条语句内容 != '':
        ## 区别处理查询语句和其他语句
        开头单词 = 单条语句内容.split()[0].lower()
        if 开头单词 in ('select','show'):
            DEF_SQL_查询和显示(SQL_CMD)                    # 调用查询语句专用函数
        else:
            R = DEF_SQL_执行(SQL_CMD)                      # 调用非查询语句函数
            if R[0] == 0:
                受影响行数 = R[1]
                INFO = f'数据库 {DB_NAME.get()} 执行SQL语句 {SQL_CMD} 成功,受影响行数:{受影响行数}'
                TEXT_数据库变动日志.insert(0.0, INFO+'\n')
                Log.info(INFO)
                tkinter.messagebox.showinfo(title='成功', message=INFO)
            else:
                ERROR = f'数据库 {DB_NAME.get()} 执行SQL语句 {SQL_CMD} 失败 {R[1]}'
                TEXT_数据库变动日志.insert(0.0, ERROR+'\n')
                Log.error(ERROR)
                tkinter.messagebox.showerror(title='失败', message=ERROR)
    else:
        ERROR = '没有输入SQL语句'
        tkinter.messagebox.showerror(title='错误', message=ERROR)

def DEF_SQL_执行多条SQL语句_事件控制(SQL_CMD):
    数据库连接对象 = DB_INFO['数据库连接']
    数据库连接对象.begin()                     # 开启事件,只对支持的事件的数据库引擎如 InnoDB 有效
    try:
        游标对象 = 数据库连接对象.cursor()     # 创建一个游标
    except Exception as e:
        ERROR = '创建游标失败' + str(e)
        print(ERROR)
        return(1, ERROR)
    else:
        try:
            ## 以分号区分语句,尝试依次执行
            for 单条语句 in SQL_CMD.split(';'):
                单条语句整理 = 单条语句.strip()           # 去掉语句首尾空格
                print("单条语句", 单条语句)
                print("单条语句整理", 单条语句整理)
                if 单条语句整理 == '':
                    print("忽略")
                else:
                    print("执行")
                    游标对象.execute(单条语句整理)
        except Exception as e:
            ERROR = str(e)
            游标对象.close()
            数据库连接对象.rollback()      # 回退,撤销修改
            print("回退 rollback()")
            return(1, ERROR)
        else:
            受影响行数 = 游标对象.rowcount
            游标对象.close()
            数据库连接对象mit()        # 提交,确认修改
            print("提交 commit()")
            return(0, 受影响行数)

## 执行用户输入的多条SQL语句(使用自定义事件控制函数)
def DEF_按钮_执行多条SQL语句():
    SQL_CMD = 文本框_命令行.get(1.0, END).replace('\n', ' ')     # 获取编写的SQL语句,回车替换为一个空格
    多条语句内容 = SQL_CMD.strip()                               # 再去掉首尾空格
    if 多条语句内容 != '':
        R = DEF_SQL_执行多条SQL语句_事件控制(多条语句内容)
        if R[0] == 0:
            INFO = f'数据库 {DB_NAME.get()} 执行多条SQL语句\n{多条语句内容}<成功>'
            TEXT_数据库变动日志.insert(0.0, INFO+'\n')
            Log.info(INFO)
            tkinter.messagebox.showinfo(title='成功', message=INFO)
        else:
            ERROR = f'数据库 {DB_NAME.get()} 执行多条SQL语句\n{多条语句内容}<失败 {R[1]}>'
            TEXT_数据库变动日志.insert(0.0, ERROR+'\n')
            Log.error(ERROR)
            tkinter.messagebox.showerror(title='失败', message=ERROR)
        FRAME_CLEAR(LabelFrame_显编框)                           # 清空显示编辑框内控件
    else:
        ERROR = '没有输入SQL语句'
        tkinter.messagebox.showerror(title='错误', message=ERROR)

本地SQL脚本文件操作框 = Frame(命令框)
本地SQL脚本文件操作框.grid(row=0,column=0)
Button(本地SQL脚本文件操作框, text='选择脚本文件', command=DEF_按钮_选择SQL脚本文本).grid(row=0, column=0, sticky='NW')
Entry(本地SQL脚本文件操作框, textvariable=DB_SQL_SCRIPT_FILE, width=55).grid(row=0, column=1)
Button(本地SQL脚本文件操作框, text='执行脚本文件', command=DEF_按钮_执行SQL脚本文件).grid(row=0, column=2, sticky='E')

命令行_按钮框 = Text(命令框)
命令行_按钮框.grid(row=1,column=0)
Button(命令行_按钮框, text='执行单条SQL语句', command=DEF_按钮_执行单条SQL语句).grid(row=0, column=0)
Button(命令行_按钮框, text='执行多条SQL语句', command=DEF_按钮_执行多条SQL语句).grid(row=0, column=1)
Button(命令行_按钮框, text=' 清屏 ', command=DEF_按钮_清屏).grid(row=0, column=2)

文本框_命令行 = Text(命令框, height=6, wrap='none')
文本框_命令行.grid(row=2,column=0,sticky='NESW')
Scrollbar_命令框_竖 = Scrollbar(命令框) 
Scrollbar_命令框_竖['command'] = 文本框_命令行.yview
Scrollbar_命令框_横 = Scrollbar(命令框) 
Scrollbar_命令框_横['command'] = 文本框_命令行.xview
Scrollbar_命令框_横['orient'] = HORIZONTAL
Scrollbar_命令框_竖.grid(row=2, column=1, sticky=S+W+E+N)
Scrollbar_命令框_横.grid(row=3, column=0, sticky=S+W+E+N)
文本框_命令行.config(xscrollcommand=Scrollbar_命令框_横.set, yscrollcommand=Scrollbar_命令框_竖.set)  # 自动设置滚动条滑动幅度

######################################################
## TKinter 主窗口布局 - TOP框 - 右侧框 - 快速命令框 ##
######################################################

def DEF_按钮_SHOW_DATABASES():
    DEF_SQL_查询和显示('SHOW DATABASES')

def DEF_按钮_SHOW_TABLES():
    DEF_SQL_查询和显示('SHOW TABLES')

def DEF_按钮_TEST():
    pass

def DEF_按钮_INSERT():
    SQL = 'INSERT INTO 表名 (列名1, 列名2) VALUES ("值1", "值2");'
    文本框_命令行.delete(0.0, END)
    文本框_命令行.insert(0.0, SQL)
    文本框_命令行.focus_set()

def DEF_按钮_DELETE():
    SQL = 'DELETE FROM 表名 WHERE 列名 = "值";'
    文本框_命令行.delete(0.0, END)
    文本框_命令行.insert(0.0, SQL)
    文本框_命令行.focus_set()

def DEF_按钮_UPDATE():
    SQL = 'UPDATE 表名 SET 列名1 = "值1", 列名2 = "值2" WHERE 列名 = "值";'
    文本框_命令行.delete(0.0, END)
    文本框_命令行.insert(0.0, SQL)
    文本框_命令行.focus_set()

def DEF_按钮_SELECT():
    SQL = 'SELECT * FROM 表名;'
    文本框_命令行.delete(0.0, END)
    文本框_命令行.insert(0.0, SQL)
    文本框_命令行.focus_set()

def DEF_按钮_LIKE():
    SQL = 'SELECT * FROM NET WHERE CONCAT(ID,MAC,IP,NAME,PS) LIKE "%51%";'
    文本框_命令行.delete(0.0, END)
    文本框_命令行.insert(0.0, SQL)
    文本框_命令行.focus_set()

自定义命令操作框 = Frame(快速命令框)
自定义命令操作框.grid(row=0,column=0)
Button(自定义命令操作框, text='SHOW DATABASES', command=DEF_按钮_SHOW_DATABASES).grid(row=0, column=0, sticky='NW')
Button(自定义命令操作框, text='SHOW TABLES', command=DEF_按钮_SHOW_TABLES).grid(row=0, column=1, sticky='NW')
Button(自定义命令操作框, text='TEST', command=DEF_按钮_TEST).grid(row=0, column=2, sticky='NW')
Button(自定义命令操作框, text='INSERT语句示例', command=DEF_按钮_INSERT).grid(row=1, column=0, sticky='NW')
Button(自定义命令操作框, text='DELETE语句示例', command=DEF_按钮_DELETE).grid(row=1, column=1, sticky='NW')
Button(自定义命令操作框, text='UPDATE语句示例', command=DEF_按钮_UPDATE).grid(row=1, column=2, sticky='NW')
Button(自定义命令操作框, text='SELECT语句示例', command=DEF_按钮_SELECT).grid(row=1, column=3, sticky='NW')
Button(自定义命令操作框, text='模糊查询语句示例', command=DEF_按钮_LIKE).grid(row=1, column=4, sticky='NW')



## 【V1.4】
## 全表全字段搜索

def DEF_按钮_表内全字段搜索():
    # 根据表名查全字段
    数据库表名 = DB_TABLE_NAME.get()
    if 数据库表名 == '':
        ERROR = '请先打开一个表'
        tkinter.messagebox.showerror(title='ERROR', message=ERROR)
    else:
        SQL_CMD = f'SELECT COLUMN_NAME FROM information_schema.columns WHERE TABLE_NAME="{数据库表名}"'
        R = DEF_SQL_查询和返回(SQL_CMD)
        if R[0] == 0:
            查询结果 = R[1]                 # (('ID',), ('A',), ('B',), ('C',), ('D',))
            print("查询结果", 查询结果)
            L_字段名 = [i[0] for i in 查询结果]
            #print("L_字段名", L_字段名)
            # 制作全表全字段搜索SQL语句:SELECT * FROM 数据库表名 WHERE 列名1 LIKE "%查找内容%" or 列名2 LIKE "%查找内容%";
            查找内容 = Entry_查找内容.get()     # 获取查找内容值
            print("查找内容", 查找内容)
            TEXT = ''
            for i in range(0, len(L_字段名)):
                if i == 0:
                    TEXT += f'{L_字段名[i]} LIKE "%{查找内容}%"'
                else:
                    TEXT += f'or {L_字段名[i]} LIKE "%{查找内容}%"'
            SQL_CMD = f'SELECT * FROM {数据库表名} WHERE {TEXT}'
            print("SQL_CMD", SQL_CMD)
            DEF_SQL_查询和显示(SQL_CMD)
        else:
            ERROR = f'查询字段信息失败:{R[1]}'

Label(常用功能框_表内全字段搜索, text='搜索内容:').grid(row=0, column=0, sticky='NW')
Entry_查找内容 = Entry(常用功能框_表内全字段搜索)
Entry_查找内容.grid(row=0, column=1, sticky='NW')
Button(常用功能框_表内全字段搜索, text='搜索', command=DEF_按钮_表内全字段搜索).grid(row=0, column=2, sticky='NW')



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

 

更多推荐

使用Python3自带GUI做个图形化操作MySQL数据库的工具