最近用windows10企业版的时候发现了一个问题,这上面的备忘录功能竟然不能打开,实在是难受,正好没事干,干脆用python写一个吧......正好初学python,也就当个练习了。要准备的工具呢就是python解释器和pyqt5了。如果需要打包在其他电脑上使用呢,可以安装pyinstaller来进行打包。话不多说,先上效果图。
UI设计就算了,以实用为准,毕竟没有审美。里面还剩余一下可以改进的地方,也懒得改了(毕竟几个小时弄出来的东西肯定还是有许多bug的),感兴趣的朋友可以自己改一下。
首先呢,我们来自定义一下这个标题栏,系统自带的属实有点丑。这里参考了晓虎虎在此
先生的(https://blog.csdn.net/qq_37386287/article/details/87900403)这篇博客以及Unlucky
先生的(https://www.jianshu.com/p/1b91fd2beb85)这篇博客,感谢!!!
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
import sqlite3
# 按钮高度
BUTTON_HEIGHT = 30
# 按钮宽度
BUTTON_WIDTH = 30
# 标题栏高度
TITLE_HEIGHT = 30
class TitleWidget(QWidget):
def __init__(self):
super().__init__()
# self.setStyleSheet("background-color:blue")
titleIcon = QPixmap("image/icon.png")
self.Icon = myLabel()
self.Icon.setPixmap(titleIcon.scaled(20, 20))
titleContent = QLabel("备忘录")
titleContent.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
titleContent.setFixedHeight(TITLE_HEIGHT)
titleContent.setObjectName("TitleContent")
self.ButtonMin = QPushButton()
self.ButtonMin.setFixedSize(QSize(BUTTON_WIDTH, BUTTON_HEIGHT))
self.ButtonMin.setObjectName("ButtonMin")
self.ButtonMax = QPushButton()
self.ButtonMax.setFixedSize(QSize(BUTTON_WIDTH, BUTTON_HEIGHT))
self.ButtonMax.setObjectName("ButtonMax")
self.ButtonRestore = QPushButton()
self.ButtonRestore.setFixedSize(QSize(BUTTON_WIDTH, BUTTON_HEIGHT))
self.ButtonRestore.setObjectName("ButtonRestore")
self.ButtonRestore.setVisible(False)
self.ButtonClose = QPushButton()
self.ButtonClose.setFixedSize(QSize(BUTTON_WIDTH, BUTTON_HEIGHT))
self.ButtonClose.setObjectName("ButtonClose")
mylayout = QHBoxLayout()
mylayout.setSpacing(0)
mylayout.setContentsMargins(5, 5, 5, 5)
mylayout.addWidget(self.Icon)
mylayout.addWidget(titleContent)
mylayout.addWidget(self.ButtonMin)
mylayout.addWidget(self.ButtonMax)
mylayout.addWidget(self.ButtonRestore)
mylayout.addWidget(self.ButtonClose)
self.setLayout(mylayout)
# QSS可写在文件中 读文件使用 这里方便大家使用直接写在代码里吧
Qss = '''
QLabel#TitleContent
{
color: #FFFFFF;
}
QPushButton#ButtonMin
{
border-image:url(image/min.png) 0 81 0 0 ;
}
QPushButton#ButtonMin:hover
{
border-image:url(image/min.png) 0 54 0 27 ;
}
QPushButton#ButtonMin:pressed
{
border-image:url(image/min.png) 0 27 0 54 ;
}
QPushButton#ButtonMax
{
border-image:url(image/max.png) 0 81 0 0 ;
}
QPushButton#ButtonMax:hover
{
border-image:url(image/max.png) 0 54 0 27 ;
}
QPushButton#ButtonMax:pressed
{
border-image:url(image/max.png) 0 27 0 54 ;
}
QPushButton#ButtonRestore
{
border-image:url(image/restore.png) 0 81 0 0 ;
}
QPushButton#ButtonRestore:hover
{
border-image:url(image/restore.png) 0 54 0 27 ;
}
QPushButton#ButtonRestore:pressed
{
border-image:url(image/restore.png) 0 27 0 54 ;
}
QPushButton#ButtonClose
{
border-image:url(image/close.png) 0 81 0 0 ;
border-top-right-radius:3 ;
}
QPushButton#ButtonClose:hover
{
border-image:url(image/close.png) 0 54 0 27 ;
border-top-right-radius:3 ;
}
QPushButton#ButtonClose:pressed
{
border-image:url(image/close.png) 0 27 0 54 ;
border-top-right-radius:3 ;
}
'''
self.setStyleSheet(Qss)
self.restorePos = None
self.restoreSize = None
self.startMovePos = None
def saveRestoreInfo(self, point, size):
self.restorePos = point
self.restoreSize = size
def getRestoreInfo(self):
return self.restorePos, self.restoreSize
然后我这里呢由于是点击备忘录三个字左边的应用图标来进行新增记录的操作,而根据上面的代码呢,我是用QLabel来放的这个图片。QLabel是没有点击操作的,所以我们需要自己定义一个QLabel来重写OnClick方法。如下。
class myLabel(QLabel):
_signal = pyqtSignal(str)
def __init__(self, parent=None):
super(myLabel, self).__init__(parent)
def mousePressEvent(self, e): ##重载一下鼠标点击事件
self._signal.emit("")
此外,还需要什么东西呢?是不是需要一个添加新记录的窗口,这个窗口呢,我们也来自定义一下。也就是我最开始上的图里面的第二张。
class InputDialog(QWidget):
def __init__(self):
super(InputDialog, self).__init__()
self.initUi()
self.id = 0
self.show()
def load_data(self, id, time, event, level):
self.time.setText(time)
self.event.setText(event)
self.level.setText(level)
self.id = id
def initUi(self):
self.setWindowTitle("新增/修改记录")
titleIcon = QIcon("image/icon_black.png")
self.setWindowIcon(titleIcon)
self.resize(300, 200)
self.center()
label1 = QLabel("时 间:")
label2 = QLabel("事 件:")
label3 = QLabel("紧急程度:")
self.time = QLineEdit()
self.event = QLineEdit()
self.level = QLineEdit()
self.level.setPlaceholderText("输入数字,越大越紧急")
self.ok_btn = QPushButton("确定")
self.cancel_btn = QPushButton("取消")
mainLayout = QVBoxLayout()
topWidget = QWidget()
botWidget = QWidget()
btnLay = QHBoxLayout()
glLay = QGridLayout()
glLay.addWidget(label1, 0, 0)
glLay.addWidget(self.time, 0, 1)
glLay.addWidget(label2, 1, 0)
glLay.addWidget(self.event, 1, 1)
glLay.addWidget(label3, 2, 0)
glLay.addWidget(self.level, 2, 1)
btnLay.addWidget(self.ok_btn, 1)
btnLay.addWidget(self.cancel_btn,1)
topWidget.setLayout(glLay)
botWidget.setLayout(btnLay)
mainLayout.addWidget(topWidget)
mainLayout.addWidget(botWidget)
self.setLayout(mainLayout)
# 控制窗口显示在屏幕中心的方法
def center(self):
# 获得窗口
qr = self.frameGeometry()
# 获得屏幕中心点
cp = QDesktopWidget().availableGeometry().center()
# 显示到屏幕中心
qr.moveCenter(cp)
self.move(qr.topLeft())
注意看这个InputDialog弹出窗口里面有个load_data方法,这个方法的用处呢,主要是在打开窗口的时候判断是新增记录还是修改记录的,如果是新增记录呢就不需要调用这个load_data方法,如果是修改记录呢那就需要传入之前的数据。
最后是我们的主程序,主程序考虑的是需要一个列表来对数据进行展示,本来想弄得对其一点好看一点的,但是对于这个QListView不是很了解,找到的许多方法都是只有简单的文字或者图片加文字,这里也用不到图片,索性就懒得弄了,直接纯文字,处理一下。至于数据存储呢,这里使用的是python自带的sqlite3数据库,轻量级,使用也很方便。
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowMinimizeButtonHint | Qt.WindowStaysOnTopHint)
self.resize(350, 500)
self.center()
AllWidget = QWidget()
# AllWidget.setStyleSheet("color:white;")
Alllayout = QVBoxLayout()
Alllayout.setSpacing(0)
Alllayout.setContentsMargins(0, 0, 0, 0)
AllWidget.setLayout(Alllayout)
self.title = TitleWidget()
self.title.setFixedWidth(self.width())
self.title.setFixedHeight(TITLE_HEIGHT)
self.setWindowOpacity(0.7) # 设置窗口透明度
# self.setAttribute(Qt.WA_TranslucentBackground) # 设置窗口背景透明
self.title.ButtonMin.clicked.connect(self.ButtonMinSlot)
self.title.ButtonMax.clicked.connect(self.ButtonMaxSlot)
self.title.ButtonRestore.clicked.connect(self.ButtonRestoreSlot)
self.title.ButtonClose.clicked.connect(self.ButtonCloseSlot)
self.title.Icon._signal.connect(self.ButtonAddNew)
centerWidget = QWidget()
ver_layout = QVBoxLayout(centerWidget)
self.list_view = QListView()
self.load_record()
self.list_view.setContextMenuPolicy(3)
self.list_view.customContextMenuRequested[QPoint].connect(self.listWidgetContext)
self.list_view.doubleClicked.connect(self.clickedlist) # listview 的点击事件
self.list_view.setStyleSheet('''
QListView{font-family:"微软雅黑"; text-size:16px;color:#005AB5; font-weight:bold;}
QListView::item{margin-top:5px;margin-bottom:5px;}
''')
self.list_view.setWordWrap(True)
ver_layout.addWidget(self.list_view)
# centerWidget 中可以随意添加自己想用的控件
# centerWidget.setStyleSheet("background-color:red")
self.Qss = '''
QMainWindow{
background:qlineargradient(spread:pad,x1:1,y1:0,x2:0,y2:0,stop:0 #3d3d3d,stop:1 #4d4d4d);
}
'''
Alllayout.addWidget(self.title)
Alllayout.addWidget(centerWidget)
self.setCentralWidget(AllWidget)
self.setStyleSheet(self.Qss)
def ButtonMinSlot(self):
self.showMinimized()
def ButtonMaxSlot(self):
self.title.ButtonMax.setVisible(False)
self.title.ButtonRestore.setVisible(True)
self.title.saveRestoreInfo(self.pos(), QSize(self.width(), self.height()))
desktopRect = QApplication.desktop().availableGeometry()
FactRect = QRect(desktopRect.x() - 3, desktopRect.y() - 3, desktopRect.width() + 6, desktopRect.height() + 6)
print(FactRect)
self.setGeometry(FactRect)
self.setFixedSize(desktopRect.width() + 6, desktopRect.height() + 6)
def ButtonRestoreSlot(self):
self.title.ButtonMax.setVisible(True)
self.title.ButtonRestore.setVisible(False)
windowPos, windowSize = self.title.getRestoreInfo()
# print(windowPos,windowSize.width(),windowSize.height())
self.setGeometry(windowPos.x(), windowPos.y(), windowSize.width(), windowSize.height())
self.setFixedSize(windowSize.width(), windowSize.height())
def ButtonCloseSlot(self):
self.close()
def paintEvent(self, event):
self.title.setFixedWidth(self.width())
def center(self):
desktop = QApplication.desktop()
self.move(desktop.width() - 400, int((desktop.height() - self.height()) / 2))
好,现在主程序的框架搭好了,我们还需要做什么呢?首先我们在打开软件的时候是不是需要主动去加载数据啊,所以接下来,我们需要搭建数据库的部分。
def load_record(self):
global data_source
data_source = []
conn = sqlite3.connect("memo.db")
cursor = conn.cursor()
# 执行一条语句,创建 user表
sql = "create table if not exists record (id integer primary key autoincrement not null , memo_time varchar(30), " \
"memo_event varchar(255), memo_level integer(5))"
cursor.execute(sql)
sql = "select * from record order by memo_level desc"
result = cursor.execute(sql)
slm = QStringListModel() # 创建mode
self.qList = [] # 添加的数组数据
for row in result:
data_source.append([str(row[0]),str(row[1]),str(row[2]),str(row[3])])
self.qList.append("时间:" + str(row[1]) + " 事件:" + str(row[2]) + " 紧急程度:" + str(row[3]) + "级")
if len(data_source) == 0:
self.qList.append("还没有记录哦,点击左上角图标添加!")
conn.commit()
slm.setStringList(self.qList) # 将数据设置到model
self.list_view.setModel(slm) ##绑定 listView 和 model
cursor.close()
conn.close()
load_record函数是一个主动访问数据库并加载数据的函数。首先使用conn = sqlite3.connect("memo.db")来连接数据库,这个函数会自动判断数据库是不是存在,不存在的话会自动创建。接下来需要创建表,创建表的时候需要判断一下是不是存在这个表,如果不存在则创建,存在的话就进行下一步。然后用result去接收数据库查询语句返回的结果,并在对这个结果进行迭代的时候将他放到我们的全局变量data_source中,方便后面的处理,打个比方,在其他函数中需要访问单条数据的内容时,我们怎么去拿到这个result的内容,result是不支持直接用数据的形式访问的,为了拿到这个数据的内容,我们不可能再去查询一遍数据库吧,这时候如果有一个全局变量可以支持数组的访问形式,那我们是不是就可以直接通过index来查找我们需要的数据。
好了,数据库的查询和可视化处理做好了,但是我们的数据表中还没有数据对不对,所以是时候完成添加备忘录的功能了。我这里呢把添加功能通过点击左上角的应用小图标来进行。在点击的时候弹出我们刚写好的input_dialog来进行输入。而主函数中的self.title.Icon._signal.connect(self.ButtonAddNew)则是对应用图标QLabel的信号插槽。链接到的函数如下。同时这里还将添加函数的功能分离成了三步(打开弹窗,数据预处理,数据插入),在点击图标时,先打开InputDialog,然后在dialog中的确定按钮被点击时需要先进行数据预处理操作self.dlg.ok_btn.clicked.connect(self.prepare_add_data),不能让用户插入不合格的数据,点击取消按钮时需要关闭弹窗。然后再进行数据库的插入操作 self.add_record(time, event, level),当然别忘了在插入数据后更新UI,我这里为了方便就直接调用了load_record函数。要注意的一点是QListView需要setWordWrap(True)这个属性,让里面的文字自动换行,不然会很丑。
def ButtonAddNew(self):
self.dlg = InputDialog()
self.dlg.setStyleSheet(self.Qss)
self.dlg.ok_btn.clicked.connect(self.prepare_add_data)
self.dlg.cancel_btn.clicked.connect(self.dlg.close)
pass
def prepare_add_data(self):
time = self.dlg.time.text()
event = self.dlg.event.text().replace(" ", "")
level = self.dlg.level.text().replace(" ", "")
if len(time.replace(" ", "")) == 0 or len(event) == 0 or len(level) == 0:
QMessageBox.information(self, '提示', '请输入正确的信息!', QMessageBox.Yes, QMessageBox.Yes)
return
self.add_record(time, event, level)
self.dlg.close()
def add_record(self, time='', event='', level=1):
conn = sqlite3.connect("memo.db")
cursor = conn.cursor()
# 插入语句
sql = "insert into record values (NULL, ?, ?, ?)"
cursor.execute(sql, (time, event, level,))
conn.commit()
cursor.close()
conn.close()
self.load_record()
数据插入的操作完成了,我们可以随便插入一条数据测试一下。然后会发现在界面上多出了刚刚添加的数据。现在还需要做什么呢?有添加那肯定得有修改和删除对吧?那这个功能放哪里呢?我首先想到的是做一个二级菜单,当鼠标在列表上右键点击的时候,弹出修改和删除的选项,当然还可以做一个双击快速修改的操作。那我们先来完成双击快速修改吧,这个简单一点,信号插槽呢已经在主函数中写好了,也就是主函数中的self.list_view.doubleClicked.connect(self.clickedlist) 这一句。至于什么styleSheet这种css基础就不用讲了吧。
def clickedlist(self, qModelIndex):
global data_source
self.dlg = InputDialog()
index = qModelIndex.row()
self.dlg.setStyleSheet(self.Qss)
self.dlg.load_data(data_source[index][0], data_source[index][1], data_source[index][2], data_source[index][3])
self.dlg.ok_btn.clicked.connect(self.prepare_update_data)
self.dlg.cancel_btn.clicked.connect(self.dlg.close)
pass
def prepare_update_data(self):
time = self.dlg.time.text()
event = self.dlg.event.text().replace(" ", "")
level = self.dlg.level.text().replace(" ", "")
if len(time.replace(" ", "")) == 0 or len(event) == 0 or len(level) == 0:
QMessageBox.information(self, '提示', '请输入正确的信息!', QMessageBox.Yes, QMessageBox.Yes)
return
self.update_record(time, event, level, self.dlg.id)
self.dlg.close()
def update_record(self, time='', event='', level=1, id=0):
conn = sqlite3.connect("memo.db")
cursor = conn.cursor()
# 更新语句
sql = "update record set memo_time = ?, memo_event = ?, memo_level = ? where id = ?"
cursor.execute(sql, (time, event, level, id,))
conn.commit()
cursor.close()
conn.close()
self.load_record()
可以看到这里也是同样的逻辑,先打开InputDialog(注意,因为这里是修改,所以我们需要调用一下他的load_data方法,load_data的数据来源就是我们之前提及到的全局变量data_source),通过他自动传入的index参数的row()方法来获取点击的对象所在的位置。然后点击确定按钮时调用数据预处理方法prepare_update_data。在update方法中判断数据合法性并调用数据库更新方法。当然,别忘了在更新完成后还要更新UI。
好了,双击更改功能现在已经实现了。接下来要实现的是右键菜单的操作。首先呢,是在主函数中的self.list_view.setContextMenuPolicy(3)以及self.list_view.customContextMenuRequested[QPoint].connect(self.listWidgetContext)这两句,就是为了设置右键菜单,接下来我们看他的实现方法。
def listWidgetContext(self, point):
global operate_index
operate_index = self.list_view.indexAt(point).row()
popMenu = QMenu()
actionA = QAction('修改', self)
actionB = QAction('删除', self)
popMenu.addAction(actionA)
popMenu.addAction(actionB)
actionA.triggered.connect(self.click_menu_to_update)
actionB.triggered.connect(self.prepare_delete_data)
popMenu.exec_(QCursor.pos())
注意看,这里的更新操作的方法跟双击的更新方法不一样哦,为什么呢,因为这里传进来的不是列表的某个值了,而是鼠标点击的相对坐标。我们需要根据这个坐标去找到他点的是列表中的第几个。所以用到了operate_index = self.list_view.indexAt(point).row()这个方法。在比较老的版本上可能是itemAt而不是indexAt,这个自己尝试一下就好了。这里又添加了一个全局变量,因为这个参数的传递懒得搞,干脆写个全局的,每次进这个方法就修改成当前的操作对象,也能达到目的。然后是click_menu_to_update方法和prepare_delete_data的内容。
def click_menu_to_update(self):
global data_source, operate_index
self.dlg = InputDialog()
self.dlg.setStyleSheet(self.Qss)
self.dlg.load_data(data_source[operate_index][0], data_source[operate_index][1],
data_source[operate_index][2], data_source[operate_index][3])
self.dlg.ok_btn.clicked.connect(self.prepare_update_data)
self.dlg.cancel_btn.clicked.connect(self.dlg.close)
def prepare_delete_data(self):
global operate_index, data_source
reply = QMessageBox.question(self,
"提示",
"确定要删除吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
print('yes')
self.delete_record(data_source[operate_index][0])
else:
pass
def delete_record(self, id=0):
conn = sqlite3.connect("memo.db")
cursor = conn.cursor()
# 执行一条语句,创建 user表
sql = "delete from record where id = ?"
cursor.execute(sql, (id,))
conn.commit()
cursor.close()
conn.close()
self.load_record()
好了,所有的代码都在这里了,实现逻辑就是点击子菜单的按钮时,先判断是点击的修改还是删除按钮,如果是修改按钮,则打开InputDialog,然后进行数据预处理,再更新数据,最后更新UI;如果是点击的删除按钮,则弹窗提示用户是否确定删除,用户点击确定则调用删除方法,点击取消则关闭弹窗。
————————————————————————————————————
最后附上项目仓库地址:memo
--The End--