I want the user to select two frequencies: one low frequency and one high frequency.
Can I make this with one QSlider?
I found this page: https://doc.qt.io/qt-5/qtwidgets-widgets-sliders-example.html but it hasn't something that can I use for this goal.
file range_slider.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys, os
from PyQt5 import QtCore, QtGui, QtWidgets
# Originated from
# https://www.mail-archive.com/pyqt#riverbankcomputing.com/msg22889.html
# Modification refered from
# https://gist.github.com/Riateche/27e36977f7d5ea72cf4f
class RangeSlider(QtWidgets.QSlider):
sliderMoved = QtCore.pyqtSignal(int, int)
""" A slider for ranges.
This class provides a dual-slider for ranges, where there is a defined
maximum and minimum, as is a normal slider, but instead of having a
single slider value, there are 2 slider values.
This class emits the same signals as the QSlider base class, with the
exception of valueChanged
"""
def __init__(self, *args):
super(RangeSlider, self).__init__(*args)
self._low = self.minimum()
self._high = self.maximum()
self.pressed_control = QtWidgets.QStyle.SC_None
self.tick_interval = 0
self.tick_position = QtWidgets.QSlider.NoTicks
self.hover_control = QtWidgets.QStyle.SC_None
self.click_offset = 0
# 0 for the low, 1 for the high, -1 for both
self.active_slider = 0
def low(self):
return self._low
def setLow(self, low:int):
self._low = low
self.update()
def high(self):
return self._high
def setHigh(self, high):
self._high = high
self.update()
def paintEvent(self, event):
# based on http://qt.gitorious.org/qt/qt/blobs/master/src/gui/widgets/qslider.cpp
painter = QtGui.QPainter(self)
style = QtWidgets.QApplication.style()
# draw groove
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
opt.siderValue = 0
opt.sliderPosition = 0
opt.subControls = QtWidgets.QStyle.SC_SliderGroove
if self.tickPosition() != self.NoTicks:
opt.subControls |= QtWidgets.QStyle.SC_SliderTickmarks
style.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt, painter, self)
groove = style.subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderGroove, self)
# drawSpan
#opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
opt.subControls = QtWidgets.QStyle.SC_SliderGroove
#if self.tickPosition() != self.NoTicks:
# opt.subControls |= QtWidgets.QStyle.SC_SliderTickmarks
opt.siderValue = 0
#print(self._low)
opt.sliderPosition = self._low
low_rect = style.subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderHandle, self)
opt.sliderPosition = self._high
high_rect = style.subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderHandle, self)
#print(low_rect, high_rect)
low_pos = self.__pick(low_rect.center())
high_pos = self.__pick(high_rect.center())
min_pos = min(low_pos, high_pos)
max_pos = max(low_pos, high_pos)
c = QtCore.QRect(low_rect.center(), high_rect.center()).center()
#print(min_pos, max_pos, c)
if opt.orientation == QtCore.Qt.Horizontal:
span_rect = QtCore.QRect(QtCore.QPoint(min_pos, c.y()-2), QtCore.QPoint(max_pos, c.y()+1))
else:
span_rect = QtCore.QRect(QtCore.QPoint(c.x()-2, min_pos), QtCore.QPoint(c.x()+1, max_pos))
#self.initStyleOption(opt)
#print(groove.x(), groove.y(), groove.width(), groove.height())
if opt.orientation == QtCore.Qt.Horizontal: groove.adjust(0, 0, -1, 0)
else: groove.adjust(0, 0, 0, -1)
if True: #self.isEnabled():
highlight = self.palette().color(QtGui.QPalette.Highlight)
painter.setBrush(QtGui.QBrush(highlight))
painter.setPen(QtGui.QPen(highlight, 0))
#painter.setPen(QtGui.QPen(self.palette().color(QtGui.QPalette.Dark), 0))
'''
if opt.orientation == QtCore.Qt.Horizontal:
self.setupPainter(painter, opt.orientation, groove.center().x(), groove.top(), groove.center().x(), groove.bottom())
else:
self.setupPainter(painter, opt.orientation, groove.left(), groove.center().y(), groove.right(), groove.center().y())
'''
#spanRect =
painter.drawRect(span_rect.intersected(groove))
#painter.drawRect(groove)
for i, value in enumerate([self._low, self._high]):
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
# Only draw the groove for the first slider so it doesn't get drawn
# on top of the existing ones every time
if i == 0:
opt.subControls = QtWidgets.QStyle.SC_SliderHandle# | QtWidgets.QStyle.SC_SliderGroove
else:
opt.subControls = QtWidgets.QStyle.SC_SliderHandle
if self.tickPosition() != self.NoTicks:
opt.subControls |= QtWidgets.QStyle.SC_SliderTickmarks
if self.pressed_control:
opt.activeSubControls = self.pressed_control
else:
opt.activeSubControls = self.hover_control
opt.sliderPosition = value
opt.sliderValue = value
style.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt, painter, self)
def mousePressEvent(self, event):
event.accept()
style = QtWidgets.QApplication.style()
button = event.button()
# In a normal slider control, when the user clicks on a point in the
# slider's total range, but not on the slider part of the control the
# control would jump the slider value to where the user clicked.
# For this control, clicks which are not direct hits will slide both
# slider parts
if button:
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
self.active_slider = -1
for i, value in enumerate([self._low, self._high]):
opt.sliderPosition = value
hit = style.hitTestComplexControl(style.CC_Slider, opt, event.pos(), self)
if hit == style.SC_SliderHandle:
self.active_slider = i
self.pressed_control = hit
self.triggerAction(self.SliderMove)
self.setRepeatAction(self.SliderNoAction)
self.setSliderDown(True)
break
if self.active_slider < 0:
self.pressed_control = QtWidgets.QStyle.SC_SliderHandle
self.click_offset = self.__pixelPosToRangeValue(self.__pick(event.pos()))
self.triggerAction(self.SliderMove)
self.setRepeatAction(self.SliderNoAction)
else:
event.ignore()
def mouseMoveEvent(self, event):
if self.pressed_control != QtWidgets.QStyle.SC_SliderHandle:
event.ignore()
return
event.accept()
new_pos = self.__pixelPosToRangeValue(self.__pick(event.pos()))
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
if self.active_slider < 0:
offset = new_pos - self.click_offset
self._high += offset
self._low += offset
if self._low < self.minimum():
diff = self.minimum() - self._low
self._low += diff
self._high += diff
if self._high > self.maximum():
diff = self.maximum() - self._high
self._low += diff
self._high += diff
elif self.active_slider == 0:
if new_pos >= self._high:
new_pos = self._high - 1
self._low = new_pos
else:
if new_pos <= self._low:
new_pos = self._low + 1
self._high = new_pos
self.click_offset = new_pos
self.update()
#self.emit(QtCore.SIGNAL('sliderMoved(int)'), new_pos)
self.sliderMoved.emit(self._low, self._high)
def __pick(self, pt):
if self.orientation() == QtCore.Qt.Horizontal:
return pt.x()
else:
return pt.y()
def __pixelPosToRangeValue(self, pos):
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
style = QtWidgets.QApplication.style()
gr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderGroove, self)
sr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderHandle, self)
if self.orientation() == QtCore.Qt.Horizontal:
slider_length = sr.width()
slider_min = gr.x()
slider_max = gr.right() - slider_length + 1
else:
slider_length = sr.height()
slider_min = gr.y()
slider_max = gr.bottom() - slider_length + 1
return style.sliderValueFromPosition(self.minimum(), self.maximum(),
pos-slider_min, slider_max-slider_min,
opt.upsideDown)
Usage:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys, os
from PyQt5 import QtCore, QtGui, QtWidgets
import range_slider
def echo(low_value, high_value):
print(low_value, high_value)
def main(argv):
app = QtWidgets.QApplication(sys.argv)
slider = range_slider.RangeSlider(QtCore.Qt.Horizontal)
slider.setMinimumHeight(30)
slider.setMinimum(0)
slider.setMaximum(255)
slider.setLow(15)
slider.setHigh(35)
slider.setTickPosition(QtWidgets.QSlider.TicksBelow)
slider.sliderMoved.connect(echo)
#QtCore.QObject.connect(slider, QtCore.SIGNAL('sliderMoved(int)'), echo)
slider.show()
slider.raise_()
app.exec_()
if __name__ == "__main__":
main(sys.argv)
link: https://github.com/Qt-Widgets/range_slider_for_Qt5_and_PyQt5/blob/master/pyqt5_ranger_slider/main.py
Related
I am developing a QTableView with ItemDelegate to display its specific widget Editor with openPersistentEditor. A pagination and filter mechanism been implemented with QSortFilterProxyModel, whenever there is a action of PageUp, PageDown or Filter changed, the program will close the previous rows of widget Editor and open new rows of widget Editor according to the "display_list" implemented.
The problem with current design is that the widget Editor will be highlighted after running the openPersistentEditor for each column cell in the display_list. I can not find a programmatic way to disable the highlighted cell.
I have tried the self.view.repaint(), self.view.update() without success. But manually bring forward/backward another window that the pyqt application MainWidonw refresh will disable the highlighted cells.
Following is the code segments of the TabWidget and CustomProxyModel been implemented.
Please note there is open_editor() and close_editor() in the class MyTabController will be triggered whenever there is a Page change action or a Filter action to be taken.
# PyQT5 and Filtering a Table Using Multiple Columns
# Refer to: https://www.onooks.com/pyqt5-and-filtering-a-table-using-multiple-columns/
import pandas as pd
from pandas_controller import *
from UI import MyTabWidget
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import QTableView, QMenu, QAction, QFileDialog
from datetime import datetime
DATE_FORMAT = "%Y-%m-%d"
# Ref: https://www.programiz.com/python-programming/datetime/strptime
page_record = 20
total_record = 0
last_page = 0
current_page = 0
model_total_record = 0
model_last_page = 0
display_list = []
def count_page_num(total_record, page_record):
page_num = int(total_record / page_record)
mod_num = total_record % page_record
if mod_num == 0 and page_num > 0:
page_num -= 1
return page_num
class MyTabController(MyTabWidget):
def __init__(self, tab, schema_df, table_name):
super().__init__()
self.view = tab.view
self.comboBox = tab.comboBox
self.lineEdit = tab.lineEdit
self.buttonImport = tab.buttonImport
self.buttonExport = tab.buttonExport
self.buttonPFirst = tab.buttonPFirst
self.buttonPLast = tab.buttonPLast
self.buttonPPrev = tab.buttonPPrev
self.buttonPNext = tab.buttonPNext
self.schema_df = schema_df[schema_df['Table'] == table_name]
self.widget_list = list(self.schema_df.Widget)
self.range_list = list(self.schema_df.Range)
self.buttonImport.clicked.connect(self.import_file)
# Event Handler
def import_file(self):
global page_record
global total_record, model_total_record
global last_page, model_last_page
print ("import_file")
self.filename, filetype = QFileDialog.getOpenFileName(self, "Open file", "../data")
if len(self.filename) == 0:
print ("Please select a file")
return
self.df = pd.read_excel(self.filename)
self.df = self.df.fillna('None')
self.header = self.df.columns.to_list()
self.indexes = self.df.index.to_list()
self.model = PandasModel(self.df, self.header, self.indexes)
self.proxy = CustomProxyModel() # Customized Filter
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.view.setAlternatingRowColors(True)
self.view.setSelectionBehavior(QTableView.SelectRows)
self.view.setWordWrap(True)
self.view.resizeColumnsToContents()
self.view.resizeRowsToContents()
self.view.setShowGrid(True)
self.view.isCornerButtonEnabled()
#self.view.setItemDelegateForColumn(0,DateDelegate(self.view))
self.view.setItemDelegate(Delegate(self.widget_list, self.range_list))
self.comboBox.addItems(["{0}".format(col) for col in self.header])
self.comboBox.currentIndexChanged.connect(self.on_comboBox_currentIndexChanged)
self.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)
self.buttonExport.clicked.connect(self.export_file)
self.buttonPFirst.clicked.connect(self.change_page)
self.buttonPLast.clicked.connect(self.change_page)
self.buttonPPrev.clicked.connect(self.change_page)
self.buttonPNext.clicked.connect(self.change_page)
self.horizontalHeader = self.view.horizontalHeader()
self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)
print ("test-1")
total_record = self.model.rowCount()
last_page = count_page_num(total_record, page_record)
model_total_record = self.model.rowCount()
model_last_page = count_page_num(total_record, page_record)
print ("model_total_record:", model_total_record)
print ("model_last_page:", model_last_page)
self.proxy.setPage()
self.open_editor()
print ("test-2")
print ("import_file completed")
def export_file(self):
output_path = '../output/'
output_filename = output_path + self.filename
df = pd.DataFrame(self.model._data, columns = self.header)
df.to_excel(output_filename)
#self.model._df.to_excel(output_filename)
def change_page(self):
global page_record
#global total_record
global last_page
global current_page
button_name = self.sender().text()
print("button clicked:", button_name)
if button_name == "First":
if current_page == 0:
take_action = False
else:
new_page = 0
take_action = True
elif button_name == "Last":
if current_page == last_page:
take_action = False
else:
new_page = last_page
take_action = True
elif button_name == "Next":
if current_page == last_page:
take_action = False
else:
new_page = current_page + 1
take_action = True
elif button_name == "Prev":
if current_page == 0:
take_action = False
else:
take_action = True
new_page = current_page - 1
if take_action == True:
print (current_page, new_page)
prev_page = current_page
current_page = new_page
self.close_editor()
self.proxy.setPage() # Trigger the invalidateFilter()
self.open_editor()
#self.view.repaint() # Refresh the Tab Widget
def open_editor(self):
global display_list
for row in display_list:
for column in range(self.model.columnCount()):
index = self.proxy.index(row, column, QModelIndex())
self.view.openPersistentEditor(index)
def close_editor(self):
global display_list
for row in display_list:
for column in range(self.model.columnCount()):
index = self.proxy.index(row, column, QModelIndex())
self.view.closePersistentEditor(index)
#QtCore.pyqtSlot(int)
def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
self.logicalIndex = logicalIndex
self.menuValues = QMenu(self)
self.signalMapper = QtCore.QSignalMapper(self)
self.comboBox.blockSignals(True)
self.comboBox.setCurrentIndex(self.logicalIndex)
self.comboBox.blockSignals(False) # True
#valuesUnique = self.model._df.iloc[:, self.logicalIndex].unique()
value_list = [str(item[self.logicalIndex]) for item in self.model._data]
valuesUnique = list(set(value_list)) # To get the unique value list
#print (valuesUnique)
actionAll = QAction("All", self)
actionAll.triggered.connect(self.on_actionAll_triggered)
self.menuValues.addAction(actionAll)
self.menuValues.addSeparator()
for actionNumber, actionName in enumerate(sorted(valuesUnique)):
action = QAction(str(actionName), self)
self.signalMapper.setMapping(action, actionNumber)
action.triggered.connect(self.signalMapper.map)
self.menuValues.addAction(action)
self.signalMapper.mapped.connect(self.on_signalMapper_mapped)
headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())
posY = headerPos.y() + self.horizontalHeader.height()
posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)
self.menuValues.exec_(QtCore.QPoint(posX, posY))
#QtCore.pyqtSlot()
def on_actionAll_triggered(self):
self.close_editor()
filterColumn = self.logicalIndex
self.proxy.setFilter("", filterColumn)
font = QtGui.QFont()
self.model.setFont(filterColumn, font)
self.open_editor()
#QtCore.pyqtSlot(int)
def on_signalMapper_mapped(self, i):
self.close_editor()
stringAction = self.signalMapper.mapping(i).text()
filterColumn = self.logicalIndex
self.proxy.setFilter(stringAction, filterColumn)
font = QtGui.QFont()
font.setBold(True)
self.model.setFont(filterColumn, font)
self.open_editor()
#QtCore.pyqtSlot(str)
def on_lineEdit_textChanged(self, text):
self.close_editor()
self.proxy.setFilter(text, self.proxy.filterKeyColumn())
self.open_editor()
#QtCore.pyqtSlot(int)
def on_comboBox_currentIndexChanged(self, index):
self.close_editor()
self.proxy.setFilterKeyColumn(index)
self.open_editor()
class Delegate(QItemDelegate):
def __init__(self, widget_list, range_list):
QItemDelegate.__init__(self)
self.widget_list = widget_list
self.range_list = range_list
def createEditor(self, parent, option, index):
column = index.column()
widget = self.widget_list[column]
range_list = self.range_list[column]
#print ("createEditor:", column, widget)
if widget == "QDateEdit":
editor = QDateEdit(parent)
editor.setCalendarPopup(True)
return editor
if widget == "QSpinBox":
editor = QSpinBox(parent)
editor.setMinimum(int(range_list[0]))
editor.setMaximum(int(range_list[1]))
return editor
if widget == "QDoubleSpinBox":
editor = QDoubleSpinBox(parent)
editor.setDecimals(3)
editor.setMinimum(float(range_list[0]))
editor.setMaximum(float(range_list[1]))
#editor.setSingleStep(0.1)
return editor
if widget == "QComboBox":
comboBox = QComboBox(parent)
comboBox.addItems(range_list)
return comboBox
# no need to check for the other columns, as Qt automatically creates a
# QLineEdit for string values and QTimeEdit for QTime values;
return super().createEditor(parent, option, index)
def setEditorData(self, editor, index):
if isinstance(editor, QDateEdit):
#dt_str = index.data(QtCore.Qt.EditRole)
dt_str = index.data(QtCore.Qt.DisplayRole)
#print ("setEditorData, dt_str:", dt_str)
dt = datetime.strptime(dt_str, DATE_FORMAT)
editor.setDate(dt)
return
super().setEditorData(editor, index)
def setModelData(self, editor, model, index):
if isinstance(editor, QDateEdit):
dt = editor.date().toPyDate()
dt_str = dt.strftime(DATE_FORMAT)
#print ("setModelData, dt_str:", dt_str)
model.setData(index, dt_str, QtCore.Qt.EditRole)
return
super().setModelData(editor, model, index)
class CustomProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._filters = dict()
self.fc = 0 # filter counter
self.filter_flag = False
self.filter_list = []
#property
def filters(self):
return self._filters
def setFilter(self, expresion, column):
global total_record
global last_page
global model_total_record
global model_last_page
if expresion:
self.filters[column] = expresion
elif column in self.filters:
del self.filters[column]
if expresion:
print ("setFilter invoke invalidateFilter()")
total_record = model_total_record # restore the original model total record
last_page = model_last_page # restore the original model last page
self.fc = 0 # reset filter counter
self.filter_flag = True # Enabled to append the self.filter_list
self.filter_list = []
self.invalidateFilter()
print ("filter counter:", self.fc)
total_record = self.fc
last_page = count_page_num(total_record, page_record)
self.fc = 0
self.filter_flag = False # Disabled to append the self.filter_list
else:
total_record = model_total_record
last_page = model_last_page
self.invalidateFilter()
def setPage(self):
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
global page_record
global total_record
#global last_page
global current_page
global display_list
#print ("filterAcceptsRow:", source_row, source_parent)
start_record = current_page * page_record
end_record = start_record + page_record
if end_record > total_record:
end_record = total_record
if len(self.filters.items()) == 0: #None Filter Items
# Filter by Current Page
#print (source_row, start_record, end_record)
if source_row < start_record:
return False
elif source_row >= end_record:
return False
else: # Filter Items
for column, expresion in self.filters.items():
text = self.sourceModel().index(source_row, column, source_parent).data()
regex = QRegExp(
expresion, Qt.CaseInsensitive, QRegExp.RegExp
)
if regex.indexIn(text) == -1:
return False
# Filter by Current Page
#print ("Filter:", source_row, start_record, end_record)
if self.filter_flag == True:
self.fc += 1
self.filter_list.append(source_row)
if self.fc < start_record:
return False
elif self.fc >= end_record:
return False
else:
target_list = self.filter_list[start_record:end_record]
if source_row not in target_list:
return False
display_list.append(source_row)
return True
Tried self.view.repaint() and self.view.update() without success.
Kivy Gui have a transition animation to switch between windows (going back and forth also) we can do it easily in kivy. But in PyQt5 I did't find out any way to transit between window (with animation) and going back and forth to a window again and again is also not working. So, is there any way to do like Kivy do transitions, going back and forth to a window easily in PyQt5.
Qt doesn't provide a similar effect on its own, but it still can be achieved using a subclass of a QStackedWidget (which behaves similarly to a QTabWidget, but without any QTabBar).
In the following example I'll show you how to implement a basic "swap" transition between two widgets that are added to a QStackedWidget, the next widget will scroll from right to left if the index is greater than the current, and vice versa.
class TransitionWidget(QtWidgets.QStackedWidget):
_nextIndex = _nextWidget = None
_orientation = QtCore.Qt.Horizontal
def __init__(self):
super().__init__()
self._animation = QtCore.QVariantAnimation(
startValue=0., endValue=1., duration=250)
self._animation.valueChanged.connect(self._aniUpdate)
self._animation.finished.connect(self._aniFinished)
self._animation.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
def setDuration(self, duration):
self._animation.setDuration(duration)
def setCurve(self, curve):
if isinstance(curve, QtCore.QEasingCurve):
self._animation.setEasingCurve(curve)
def setOrientation(self, orientation):
self._orientation = orientation
def getRange(self, prevIndex, nextIndex):
rect = self.rect()
currentStart = nextEnd = QtCore.QPoint()
if self._orientation == QtCore.Qt.Horizontal:
if prevIndex < nextIndex:
currentEnd = QtCore.QPoint(-rect.width(), 0)
nextStart = QtCore.QPoint(rect.width(), 0)
else:
currentEnd = QtCore.QPoint(rect.width(), 0)
nextStart = QtCore.QPoint(-rect.width(), 0)
else:
if prevIndex < nextIndex:
currentEnd = QtCore.QPoint(0, -rect.width())
nextStart = QtCore.QPoint(0, rect.width())
else:
currentEnd = QtCore.QPoint(0, rect.width())
nextStart = QtCore.QPoint(0, -rect.width())
return currentStart, currentEnd, nextStart, nextEnd
def setCurrentIndex(self, index):
if index == self.currentIndex():
return
# prepare the next widget changes
if self._nextWidget is not None:
self._nextWidget.hide()
self._nextIndex = index
self._nextWidget = self.widget(index)
self._nextWidget.show()
rect = self.rect()
rect.translate(self.rect().topRight())
self._nextWidget.setGeometry(rect)
self._nextWidget.raise_()
self._animation.start()
def _aniFinished(self):
super().setCurrentIndex(self._nextIndex)
self._nextIndex = self._nextWidget = None
def _aniUpdate(self, value):
if not self._animation.state():
return
currentStart, currentEnd, nextStart, nextEnd = self.getRange(self.currentIndex(), self._nextIndex)
rect = self.rect()
self.currentWidget().setGeometry(rect.translated(QtCore.QLineF(currentStart, currentEnd).pointAt(value).toPoint()))
self._nextWidget.setGeometry(rect.translated(QtCore.QLineF(nextStart, nextEnd).pointAt(value).toPoint()))
self.update()
Example code:
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
mainWidget = QtWidgets.QWidget()
mainLayout = QtWidgets.QHBoxLayout(mainWidget)
transitionWidget = TransitionWidget()
mainLayout.addWidget(transitionWidget)
pageCount = 10
for page in range(pageCount):
widget = QtWidgets.QWidget()
layout = QtWidgets.QGridLayout(widget)
pageLabel = QtWidgets.QLabel('Page {}'.format(page + 1))
layout.addWidget(pageLabel, 0, 0, 1, 2)
prevBtn = QtWidgets.QPushButton('Previous')
if not page:
prevBtn.setEnabled(False)
layout.addWidget(prevBtn)
nextBtn = QtWidgets.QPushButton('Next')
layout.addWidget(nextBtn)
if page == pageCount - 1:
nextBtn.setEnabled(False)
transitionWidget.addWidget(widget)
prevBtn.clicked.connect(lambda _, page=page: transitionWidget.setCurrentIndex(page - 1))
nextBtn.clicked.connect(lambda _, page=page: transitionWidget.setCurrentIndex(page + 1))
sep = QtWidgets.QFrame(frameShape=QtWidgets.QFrame.VLine)
mainLayout.addWidget(sep)
orientationCombo = QtWidgets.QComboBox()
orientationLayout = QtWidgets.QFormLayout()
mainLayout.addLayout(orientationLayout)
orientationCombo.addItems(['Horizontal', 'Vertical'])
orientationCombo.currentIndexChanged.connect(lambda o: transitionWidget.setOrientation(o + 1))
orientationLayout.addRow('Orientation', orientationCombo)
durationSpin = QtWidgets.QSpinBox(minimum=50, maximum=1000, singleStep=50, suffix='ms')
orientationLayout.addRow('Duration', durationSpin)
durationSpin.setValue(transitionWidget._animation.duration())
durationSpin.valueChanged.connect(transitionWidget.setDuration)
mainWidget.show()
sys.exit(app.exec_())
I am sorry for repetitively asking questions regarding pyqt, but the limited resources on it force me to.
I have been trying to implement a resize function for my circle shape, that is an extension of QGraphicsItem, which has an ellipse and a centered text. I am able to resize my shape as desired, but the shape takes a while to catch up to the mouse, i.e on switching directions the circle continues to increase but takes a while to switch directions, moreover the circle resizes with an anchor at the top left corner of the bounding rect.
See this question for a bit of code background
def updateHandlesPos(self):
s = self.handleSize
b = self.boundingRect()
self.handles[self.handleTopLeft] = QRectF(b.left(), b.top(), s, s)
self.handles[self.handleTopMiddle] = QRectF(b.center().x() - s / 2, b.top(), s, s)
self.handles[self.handleTopRight] = QRectF(b.right() - s, b.top(), s, s)
self.handles[self.handleMiddleLeft] = QRectF(b.left(), b.center().y() - s / 2, s, s)
self.handles[self.handleMiddleRight] = QRectF(b.right() - s, b.center().y() - s / 2, s, s)
self.handles[self.handleBottomLeft] = QRectF(b.left(), b.bottom() - s, s, s)
self.handles[self.handleBottomMiddle] = QRectF(b.center().x() - s / 2, b.bottom() - s, s, s)
self.handles[self.handleBottomRight] = QRectF(b.right() - s, b.bottom() - s, s, s)
def interactiveResize(self, mousePos):
self.prepareGeometryChange()
if self.handleSelected in [self.handleTopLeft,
self.handleTopRight,
self.handleBottomLeft,
self.handleBottomRight,
self.handleTopMiddle,
self.handleBottomMiddle,
self.handleMiddleLeft,
self.handleMiddleRight]:
self.radius += (mousePos.y() + mousePos.x() + self.mousePressPos.x() - self.mousePressPos.y())/64
self.setPos(self.x(),self.y())
self.update()
self.updateHandlesPos()
Since the OP has not provided an MRE then I have created an example from scratch. The logic is to track the changes of the items and according to that calculate the new geometry and establish the new position of the other items.
from PyQt5 import QtWidgets, QtGui, QtCore
class GripItem(QtWidgets.QGraphicsPathItem):
circle = QtGui.QPainterPath()
circle.addEllipse(QtCore.QRectF(-5, -5, 10, 10))
square = QtGui.QPainterPath()
square.addRect(QtCore.QRectF(-10, -10, 20, 20))
def __init__(self, annotation_item, index):
super(GripItem, self).__init__()
self.m_annotation_item = annotation_item
self.m_index = index
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
self.setPen(QtGui.QPen(QtGui.QColor("green"), 2))
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setAcceptHoverEvents(True)
self.setZValue(11)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
def hoverEnterEvent(self, event):
self.setPath(GripItem.square)
self.setBrush(QtGui.QColor("red"))
super(GripItem, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
super(GripItem, self).hoverLeaveEvent(event)
def mouseReleaseEvent(self, event):
self.setSelected(False)
super(GripItem, self).mouseReleaseEvent(event)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
self.m_annotation_item.movePoint(self.m_index, value)
return super(GripItem, self).itemChange(change, value)
class DirectionGripItem(GripItem):
def __init__(self, annotation_item, direction=QtCore.Qt.Horizontal, parent=None):
super(DirectionGripItem, self).__init__(annotation_item, parent)
self._direction = direction
#property
def direction(self):
return self._direction
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
p = QtCore.QPointF(self.pos())
if self.direction == QtCore.Qt.Horizontal:
p.setX(value.x())
elif self.direction == QtCore.Qt.Vertical:
p.setY(value.y())
self.m_annotation_item.movePoint(self.m_index, p)
return p
return super(DirectionGripItem, self).itemChange(change, value)
class CircleAnnotation(QtWidgets.QGraphicsEllipseItem):
def __init__(self, radius=1, parent=None):
super(CircleAnnotation, self).__init__(parent)
self.setZValue(11)
self.m_items = []
self.setPen(QtGui.QPen(QtGui.QColor("green"), 4))
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setAcceptHoverEvents(True)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
self._radius = radius
self.update_rect()
#property
def radius(self):
return self._radius
#radius.setter
def radius(self, r):
if r <= 0:
raise ValueError("radius must be positive")
self._radius = r
self.update_rect()
self.add_grip_items()
self.update_items_positions()
def update_rect(self):
rect = QtCore.QRectF(0, 0, 2 * self.radius, 2 * self.radius)
rect.moveCenter(self.rect().center())
self.setRect(rect)
def add_grip_items(self):
if self.scene() and not self.m_items:
for i, (direction) in enumerate(
(
QtCore.Qt.Vertical,
QtCore.Qt.Horizontal,
QtCore.Qt.Vertical,
QtCore.Qt.Horizontal,
)
):
item = DirectionGripItem(self, direction, i)
self.scene().addItem(item)
self.m_items.append(item)
def movePoint(self, i, p):
if 0 <= i < min(4, len(self.m_items)):
item_selected = self.m_items[i]
lp = self.mapFromScene(p)
self._radius = (lp - self.rect().center()).manhattanLength()
k = self.indexOf(lp)
if k is not None:
self.m_items = [item for item in self.m_items if not item.isSelected()]
self.m_items.insert(k, item_selected)
self.update_items_positions([k])
self.update_rect()
def update_items_positions(self, index_no_updates=None):
index_no_updates = index_no_updates or []
for i, (item, direction) in enumerate(
zip(
self.m_items,
(
QtCore.Qt.Vertical,
QtCore.Qt.Horizontal,
QtCore.Qt.Vertical,
QtCore.Qt.Horizontal,
),
),
):
item.m_index = i
if i not in index_no_updates:
pos = self.mapToScene(self.point(i))
item = self.m_items[i]
item._direction = direction
item.setEnabled(False)
item.setPos(pos)
item.setEnabled(True)
def indexOf(self, p):
for i in range(4):
if p == self.point(i):
return i
def point(self, index):
if 0 <= index < 4:
return [
QtCore.QPointF(0, -self.radius),
QtCore.QPointF(self.radius, 0),
QtCore.QPointF(0, self.radius),
QtCore.QPointF(-self.radius, 0),
][index]
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
self.update_items_positions()
return
if change == QtWidgets.QGraphicsItem.ItemSceneHasChanged:
self.add_grip_items()
self.update_items_positions()
return
return super(CircleAnnotation, self).itemChange(change, value)
def hoverEnterEvent(self, event):
self.setBrush(QtGui.QColor(255, 0, 0, 100))
super(CircleAnnotation, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
super(CircleAnnotation, self).hoverLeaveEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QtWidgets.QGraphicsScene()
view = QtWidgets.QGraphicsView(scene)
view.setRenderHints(QtGui.QPainter.Antialiasing)
item = CircleAnnotation()
item.radius = 100
scene.addItem(item)
view.showMaximized()
sys.exit(app.exec_())
I am trying to build an application using PyQt. A part of the application runs a thread which takes some time to complete. How can I add a waiting indicator (preferably circular) to indicate running of the process?
I know I can go with a progress bar, or perhaps a splash screen. I have looked into both but I am trying to opt for one of them only as a last resort. Can someone please help me with this?
Thank You
I have converted the code from the QtWaitingSpinner from C ++ to PyQt4/PyQt5
from math import ceil
"""from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *"""
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class QtWaitingSpinner(QWidget):
mColor = QColor(Qt.gray)
mRoundness = 100.0
mMinimumTrailOpacity = 31.4159265358979323846
mTrailFadePercentage = 50.0
mRevolutionsPerSecond = 1.57079632679489661923
mNumberOfLines = 20
mLineLength = 10
mLineWidth = 2
mInnerRadius = 20
mCurrentCounter = 0
mIsSpinning = False
def __init__(self, centerOnParent=True, disableParentWhenSpinning=True, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
self.mCenterOnParent = centerOnParent
self.mDisableParentWhenSpinning = disableParentWhenSpinning
self.initialize()
def initialize(self):
self.timer = QTimer(self)
self.timer.timeout.connect(self.rotate)
self.updateSize()
self.updateTimer()
self.hide()
#pyqtSlot()
def rotate(self):
self.mCurrentCounter += 1
if self.mCurrentCounter > self.numberOfLines():
self.mCurrentCounter = 0
self.update()
def updateSize(self):
size = (self.mInnerRadius + self.mLineLength) * 2
self.setFixedSize(size, size)
def updateTimer(self):
self.timer.setInterval(1000 / (self.mNumberOfLines * self.mRevolutionsPerSecond))
def updatePosition(self):
if self.parentWidget() and self.mCenterOnParent:
self.move(self.parentWidget().width() / 2 - self.width() / 2,
self.parentWidget().height() / 2 - self.height() / 2)
def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines):
distance = primary - current
if distance < 0:
distance += totalNrOfLines
return distance
def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, color):
if countDistance == 0:
return color
minAlphaF = minOpacity / 100.0
distanceThreshold = ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)
if countDistance > distanceThreshold:
color.setAlphaF(minAlphaF)
else:
alphaDiff = self.mColor.alphaF() - minAlphaF
gradient = alphaDiff / distanceThreshold + 1.0
resultAlpha = color.alphaF() - gradient * countDistance
resultAlpha = min(1.0, max(0.0, resultAlpha))
color.setAlphaF(resultAlpha)
return color
def paintEvent(self, event):
self.updatePosition()
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
if self.mCurrentCounter > self.mNumberOfLines:
self.mCurrentCounter = 0
painter.setPen(Qt.NoPen)
for i in range(self.mNumberOfLines):
painter.save()
painter.translate(self.mInnerRadius + self.mLineLength,
self.mInnerRadius + self.mLineLength)
rotateAngle = 360.0 * i / self.mNumberOfLines
painter.rotate(rotateAngle)
painter.translate(self.mInnerRadius, 0)
distance = self.lineCountDistanceFromPrimary(i, self.mCurrentCounter,
self.mNumberOfLines)
color = self.currentLineColor(distance, self.mNumberOfLines,
self.mTrailFadePercentage, self.mMinimumTrailOpacity, self.mColor)
painter.setBrush(color)
painter.drawRoundedRect(QRect(0, -self.mLineWidth // 2, self.mLineLength, self.mLineLength),
self.mRoundness, Qt.RelativeSize)
painter.restore()
def start(self):
self.updatePosition()
self.mIsSpinning = True
self.show()
if self.parentWidget() and self.mDisableParentWhenSpinning:
self.parentWidget().setEnabled(False)
if not self.timer.isActive():
self.timer.start()
self.mCurrentCounter = 0
def stop(self):
self.mIsSpinning = False
self.hide()
if self.parentWidget() and self.mDisableParentWhenSpinning:
self.parentWidget().setEnabled(True)
if self.timer.isActive():
self.timer.stop()
self.mCurrentCounter = 0
def setNumberOfLines(self, lines):
self.mNumberOfLines = lines
self.updateTimer()
def setLineLength(self, length):
self.mLineLength = length
self.updateSize()
def setLineWidth(self, width):
self.mLineWidth = width
self.updateSize()
def setInnerRadius(self, radius):
self.mInnerRadius = radius
self.updateSize()
def color(self):
return self.mColor
def roundness(self):
return self.mRoundness
def minimumTrailOpacity(self):
return self.mMinimumTrailOpacity
def trailFadePercentage(self):
return self.mTrailFadePercentage
def revolutionsPersSecond(self):
return self.mRevolutionsPerSecond
def numberOfLines(self):
return self.mNumberOfLines
def lineLength(self):
return self.mLineLength
def lineWidth(self):
return self.mLineWidth
def innerRadius(self):
return self.mInnerRadius
def isSpinning(self):
return self.mIsSpinning
def setRoundness(self, roundness):
self.mRoundness = min(0.0, max(100, roundness))
def setColor(self, color):
self.mColor = color
def setRevolutionsPerSecond(self, revolutionsPerSecond):
self.mRevolutionsPerSecond = revolutionsPerSecond
self.updateTimer()
def setTrailFadePercentage(self, trail):
self.mTrailFadePercentage = trail
def setMinimumTrailOpacity(self, minimumTrailOpacity):
self.mMinimumTrailOpacity = minimumTrailOpacity
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
dial = QDialog()
w = QtWaitingSpinner(dial)
dial.show()
w.start()
QTimer.singleShot(1000, w.stop)
sys.exit(app.exec_())
In the following link you can find a complete example
In addition to the above response...
If you are using Qt Designer you'll need to create a QWidget and promote it to a QtWaitingSpinner. You can follow this for promoting QWidgets. In this case, your Promoted Class name is QtWaitingSpinner and Header file is the file where the QtWaitingSpinner class is located.
Once your GUI is set you can call
your_QtWaitingSpinnerObject.start()
I want to create textEdit with line number on the left side in PyQt like Notepad++. I tried this adding another textEdit but scrolling is stuck. I searched and found this question, but there is no good solution for it.
Is this what you are looking for CodeEditor example in pyqt based on the c++ http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html
Putting it together for python3 (Im using PyQt4 not 5 but I guess it is similar) (and using QPlainTextEdit not QTextEdit see QTextEdit vs QPlainTextEdit ):
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import numpy as np
class LineNumberArea(QWidget):
def __init__(self, editor):
super().__init__(editor)
self.myeditor = editor
def sizeHint(self):
return Qsize(self.editor.lineNumberAreaWidth(), 0)
def paintEvent(self, event):
self.myeditor.lineNumberAreaPaintEvent(event)
class CodeEditor(QPlainTextEdit):
def __init__(self):
super().__init__()
self.lineNumberArea = LineNumberArea(self)
self.connect(self, SIGNAL('blockCountChanged(int)'), self.updateLineNumberAreaWidth)
self.connect(self, SIGNAL('updateRequest(QRect,int)'), self.updateLineNumberArea)
self.connect(self, SIGNAL('cursorPositionChanged()'), self.highlightCurrentLine)
self.updateLineNumberAreaWidth(0)
def lineNumberAreaWidth(self):
digits = 1
count = max(1, self.blockCount())
while count >= 10:
count /= 10
digits += 1
space = 3 + self.fontMetrics().width('9') * digits
return space
def updateLineNumberAreaWidth(self, _):
self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)
def updateLineNumberArea(self, rect, dy):
if dy:
self.lineNumberArea.scroll(0, dy)
else:
self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(),
rect.height())
if rect.contains(self.viewport().rect()):
self.updateLineNumberAreaWidth(0)
def resizeEvent(self, event):
super().resizeEvent(event)
cr = self.contentsRect();
self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(),
self.lineNumberAreaWidth(), cr.height()))
def lineNumberAreaPaintEvent(self, event):
mypainter = QPainter(self.lineNumberArea)
mypainter.fillRect(event.rect(), Qt.lightGray)
block = self.firstVisibleBlock()
blockNumber = block.blockNumber()
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
bottom = top + self.blockBoundingRect(block).height()
# Just to make sure I use the right font
height = self.fontMetrics().height()
while block.isValid() and (top <= event.rect().bottom()):
if block.isVisible() and (bottom >= event.rect().top()):
number = str(blockNumber + 1)
mypainter.setPen(Qt.black)
mypainter.drawText(0, top, self.lineNumberArea.width(), height,
Qt.AlignRight, number)
block = block.next()
top = bottom
bottom = top + self.blockBoundingRect(block).height()
blockNumber += 1
def highlightCurrentLine(self):
extraSelections = []
if not self.isReadOnly():
selection = QTextEdit.ExtraSelection()
lineColor = QColor(Qt.yellow).lighter(160)
selection.format.setBackground(lineColor)
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
selection.cursor = self.textCursor()
selection.cursor.clearSelection()
extraSelections.append(selection)
self.setExtraSelections(extraSelections)
if __name__ == "__main__":
app = QApplication(sys.argv)
txt = CodeEditor()
txt.show()
sys.exit(app.exec_())
It's my code of PyQt5 and python3 which combined #Dan-Dev and #Axel Schneider. You can directly run it, or simply import use code from QCodeEditor import QCodeEditor.
#!/usr/bin/python3
# QcodeEditor.py by acbetter.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import Qt, QRect, QSize
from PyQt5.QtWidgets import QWidget, QPlainTextEdit, QTextEdit
from PyQt5.QtGui import QColor, QPainter, QTextFormat
class QLineNumberArea(QWidget):
def __init__(self, editor):
super().__init__(editor)
self.codeEditor = editor
def sizeHint(self):
return QSize(self.editor.lineNumberAreaWidth(), 0)
def paintEvent(self, event):
self.codeEditor.lineNumberAreaPaintEvent(event)
class QCodeEditor(QPlainTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.lineNumberArea = QLineNumberArea(self)
self.blockCountChanged.connect(self.updateLineNumberAreaWidth)
self.updateRequest.connect(self.updateLineNumberArea)
self.cursorPositionChanged.connect(self.highlightCurrentLine)
self.updateLineNumberAreaWidth(0)
def lineNumberAreaWidth(self):
digits = 1
max_value = max(1, self.blockCount())
while max_value >= 10:
max_value /= 10
digits += 1
space = 3 + self.fontMetrics().width('9') * digits
return space
def updateLineNumberAreaWidth(self, _):
self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)
def updateLineNumberArea(self, rect, dy):
if dy:
self.lineNumberArea.scroll(0, dy)
else:
self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height())
if rect.contains(self.viewport().rect()):
self.updateLineNumberAreaWidth(0)
def resizeEvent(self, event):
super().resizeEvent(event)
cr = self.contentsRect()
self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(), self.lineNumberAreaWidth(), cr.height()))
def highlightCurrentLine(self):
extraSelections = []
if not self.isReadOnly():
selection = QTextEdit.ExtraSelection()
lineColor = QColor(Qt.yellow).lighter(160)
selection.format.setBackground(lineColor)
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
selection.cursor = self.textCursor()
selection.cursor.clearSelection()
extraSelections.append(selection)
self.setExtraSelections(extraSelections)
def lineNumberAreaPaintEvent(self, event):
painter = QPainter(self.lineNumberArea)
painter.fillRect(event.rect(), Qt.lightGray)
block = self.firstVisibleBlock()
blockNumber = block.blockNumber()
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
bottom = top + self.blockBoundingRect(block).height()
# Just to make sure I use the right font
height = self.fontMetrics().height()
while block.isValid() and (top <= event.rect().bottom()):
if block.isVisible() and (bottom >= event.rect().top()):
number = str(blockNumber + 1)
painter.setPen(Qt.black)
painter.drawText(0, top, self.lineNumberArea.width(), height, Qt.AlignRight, number)
block = block.next()
top = bottom
bottom = top + self.blockBoundingRect(block).height()
blockNumber += 1
if __name__ == '__main__':
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
codeEditor = QCodeEditor()
codeEditor.show()
sys.exit(app.exec_())
It runs like this.
By the way, if you want use it in your Qt Designer, you should do it like this and place the *.ui file in the same directory of your QCodeEditor.py file's path unless you set the environment variable. And, you need convert your *.ui file to *.py file by the command pyuic5 -x *.ui -o *.py. Hope Helpful~
here a version for PyQt5 (with Menu etc ...)
#!/usr/bin/python3
# -- coding: utf-8 --
from PyQt5.QtWidgets import QPlainTextEdit, QWidget, QVBoxLayout, QApplication, QFileDialog, QMessageBox, QHBoxLayout, \
QFrame, QTextEdit, QToolBar, QComboBox, QLabel, QAction, QLineEdit, QToolButton, QMenu, QMainWindow
from PyQt5.QtGui import QIcon, QPainter, QTextFormat, QColor, QTextCursor, QKeySequence, QClipboard, QTextCharFormat, QPalette
from PyQt5.QtCore import Qt, QVariant, QRect, QDir, QFile, QFileInfo, QTextStream, QRegExp, QSettings
import sys, os
lineBarColor = QColor("#ACDED5")
lineHighlightColor = QColor("#ACDED5")
class NumberBar(QWidget):
def __init__(self, parent = None):
super(NumberBar, self).__init__(parent)
self.editor = parent
layout = QVBoxLayout()
self.setLayout(layout)
self.editor.blockCountChanged.connect(self.update_width)
self.editor.updateRequest.connect(self.update_on_scroll)
self.update_width('1')
def update_on_scroll(self, rect, scroll):
if self.isVisible():
if scroll:
self.scroll(0, scroll)
else:
self.update()
def update_width(self, string):
width = self.fontMetrics().width(str(string)) + 10
if self.width() != width:
self.setFixedWidth(width)
def paintEvent(self, event):
if self.isVisible():
block = self.editor.firstVisibleBlock()
height = self.fontMetrics().height()
number = block.blockNumber()
painter = QPainter(self)
painter.fillRect(event.rect(), lineBarColor)
painter.drawRect(0, 0, event.rect().width() - 1, event.rect().height() - 1)
font = painter.font()
current_block = self.editor.textCursor().block().blockNumber() + 1
condition = True
while block.isValid() and condition:
block_geometry = self.editor.blockBoundingGeometry(block)
offset = self.editor.contentOffset()
block_top = block_geometry.translated(offset).top()
number += 1
rect = QRect(0, block_top, self.width() - 5, height)
if number == current_block:
font.setBold(True)
else:
font.setBold(False)
painter.setFont(font)
painter.drawText(rect, Qt.AlignRight, '%i'%number)
if block_top > event.rect().bottom():
condition = False
block = block.next()
painter.end()
class myEditor(QMainWindow):
def __init__(self, parent = None):
super(myEditor, self).__init__(parent)
self.MaxRecentFiles = 5
self.windowList = []
self.recentFileActs = []
self.setAttribute(Qt.WA_DeleteOnClose)
# Editor Widget ...
QIcon.setThemeName('Faenza-Dark')
self.editor = QPlainTextEdit()
self.editor.setStyleSheet(stylesheet2(self))
self.editor.setFrameStyle(QFrame.NoFrame)
self.editor.setTabStopWidth(14)
self.extra_selections = []
self.fname = ""
self.filename = ""
# Line Numbers ...
self.numbers = NumberBar(self.editor)
self.createActions()
# Laying out...
layoutH = QHBoxLayout()
layoutH.setSpacing(1.5)
layoutH.addWidget(self.numbers)
layoutH.addWidget(self.editor)
### begin toolbar
tb = QToolBar(self)
tb.setWindowTitle("File Toolbar")
self.newAct = QAction("&New", self, shortcut=QKeySequence.New,
statusTip="Create a new file", triggered=self.newFile)
self.newAct.setIcon(QIcon.fromTheme("document-new"))
self.openAct = QAction("&Open", self, shortcut=QKeySequence.Open,
statusTip="open file", triggered=self.openFile)
self.openAct.setIcon(QIcon.fromTheme("document-open"))
self.saveAct = QAction("&Save", self, shortcut=QKeySequence.Save,
statusTip="save file", triggered=self.fileSave)
self.saveAct.setIcon(QIcon.fromTheme("document-save"))
self.saveAsAct = QAction("&Save as ...", self, shortcut=QKeySequence.SaveAs,
statusTip="save file as ...", triggered=self.fileSaveAs)
self.saveAsAct.setIcon(QIcon.fromTheme("document-save-as"))
self.exitAct = QAction("Exit", self, shortcut=QKeySequence.Quit,
toolTip="Exit", triggered=self.handleQuit)
self.exitAct.setIcon(QIcon.fromTheme("application-exit"))
### find / replace toolbar
self.tbf = QToolBar(self)
self.tbf.setWindowTitle("Find Toolbar")
self.findfield = QLineEdit()
self.findfield.addAction(QIcon.fromTheme("edit-find"), QLineEdit.LeadingPosition)
self.findfield.setClearButtonEnabled(True)
self.findfield.setFixedWidth(150)
self.findfield.setPlaceholderText("find")
self.findfield.setToolTip("press RETURN to find")
self.findfield.setText("")
ft = self.findfield.text()
self.findfield.returnPressed.connect(self.findText)
self.tbf.addWidget(self.findfield)
self.replacefield = QLineEdit()
self.replacefield.addAction(QIcon.fromTheme("edit-find-and-replace"), QLineEdit.LeadingPosition)
self.replacefield.setClearButtonEnabled(True)
self.replacefield.setFixedWidth(150)
self.replacefield.setPlaceholderText("replace with")
self.replacefield.setToolTip("press RETURN to replace the first")
self.replacefield.returnPressed.connect(self.replaceOne)
self.tbf.addSeparator()
self.tbf.addWidget(self.replacefield)
self.tbf.addSeparator()
self.tbf.addAction("replace all", self.replaceAll)
self.tbf.addSeparator()
layoutV = QVBoxLayout()
bar=self.menuBar()
self.filemenu=bar.addMenu("File")
self.separatorAct = self.filemenu.addSeparator()
self.filemenu.addAction(self.newAct)
self.filemenu.addAction(self.openAct)
self.filemenu.addAction(self.saveAct)
self.filemenu.addAction(self.saveAsAct)
self.filemenu.addSeparator()
for i in range(self.MaxRecentFiles):
self.filemenu.addAction(self.recentFileActs[i])
self.updateRecentFileActions()
self.filemenu.addSeparator()
self.filemenu.addAction(self.exitAct)
bar.setStyleSheet(stylesheet2(self))
editmenu = bar.addMenu("Edit")
editmenu.addAction(QAction(QIcon.fromTheme('edit-copy'), "Copy", self, triggered = self.editor.copy, shortcut = QKeySequence.Copy))
editmenu.addAction(QAction(QIcon.fromTheme('edit-cut'), "Cut", self, triggered = self.editor.cut, shortcut = QKeySequence.Cut))
editmenu.addAction(QAction(QIcon.fromTheme('edit-paste'), "Paste", self, triggered = self.editor.paste, shortcut = QKeySequence.Paste))
editmenu.addAction(QAction(QIcon.fromTheme('edit-delete'), "Delete", self, triggered = self.editor.cut, shortcut = QKeySequence.Delete))
editmenu.addSeparator()
editmenu.addAction(QAction(QIcon.fromTheme('edit-select-all'), "Select All", self, triggered = self.editor.selectAll, shortcut = QKeySequence.SelectAll))
layoutV.addWidget(bar)
layoutV.addWidget(self.tbf)
layoutV.addLayout(layoutH)
### main window
mq = QWidget(self)
mq.setLayout(layoutV)
self.setCentralWidget(mq)
# Event Filter ...
self.installEventFilter(self)
self.editor.setFocus()
self.cursor = QTextCursor()
self.editor.setPlainText("hello")
self.editor.moveCursor(self.cursor.End)
self.editor.document().modificationChanged.connect(self.setWindowModified)
# Brackets ExtraSelection ...
self.left_selected_bracket = QTextEdit.ExtraSelection()
self.right_selected_bracket = QTextEdit.ExtraSelection()
def createActions(self):
for i in range(self.MaxRecentFiles):
self.recentFileActs.append(
QAction(self, visible=False,
triggered=self.openRecentFile))
def openRecentFile(self):
action = self.sender()
if action:
if (self.maybeSave()):
self.openFileOnStart(action.data())
### New File
def newFile(self):
if self.maybeSave():
self.editor.clear()
self.editor.setPlainText("")
self.filename = ""
self.setModified(False)
self.editor.moveCursor(self.cursor.End)
### open File
def openFileOnStart(self, path=None):
if path:
inFile = QFile(path)
if inFile.open(QFile.ReadWrite | QFile.Text):
text = inFile.readAll()
try:
# Python v3.
text = str(text, encoding = 'utf8')
except TypeError:
# Python v2.
text = str(text)
self.editor.setPlainText(text)
self.filename = path
self.setModified(False)
self.fname = QFileInfo(path).fileName()
self.setWindowTitle(self.fname + "[*]")
self.document = self.editor.document()
self.setCurrentFile(self.filename)
### open File
def openFile(self, path=None):
if self.maybeSave():
if not path:
path, _ = QFileDialog.getOpenFileName(self, "Open File", QDir.homePath() + "/Documents/",
"Text Files (*.txt *.csv *.py);;All Files (*.*)")
if path:
inFile = QFile(path)
if inFile.open(QFile.ReadWrite | QFile.Text):
text = inFile.readAll()
try:
# Python v3.
text = str(text, encoding = 'utf8')
except TypeError:
# Python v2.
text = str(text)
self.editor.setPlainText(text)
self.filename = path
self.setModified(False)
self.fname = QFileInfo(path).fileName()
self.setWindowTitle(self.fname + "[*]")
self.document = self.editor.document()
self.setCurrentFile(self.filename)
def fileSave(self):
if (self.filename != ""):
file = QFile(self.filename)
print(self.filename)
if not file.open( QFile.WriteOnly | QFile.Text):
QMessageBox.warning(self, "Error",
"Cannot write file %s:\n%s." % (self.filename, file.errorString()))
return
outstr = QTextStream(file)
QApplication.setOverrideCursor(Qt.WaitCursor)
outstr << self.editor.toPlainText()
QApplication.restoreOverrideCursor()
self.setModified(False)
self.fname = QFileInfo(self.filename).fileName()
self.setWindowTitle(self.fname + "[*]")
self.setCurrentFile(self.filename)
else:
self.fileSaveAs()
### save File
def fileSaveAs(self):
fn, _ = QFileDialog.getSaveFileName(self, "Save as...", self.filename,
"Python files (*.py)")
if not fn:
print("Error saving")
return False
lfn = fn.lower()
if not lfn.endswith('.py'):
fn += '.py'
self.filename = fn
self.fname = os.path.splitext(str(fn))[0].split("/")[-1]
return self.fileSave()
def closeEvent(self, e):
if self.maybeSave():
e.accept()
else:
e.ignore()
### ask to save
def maybeSave(self):
if not self.isModified():
return True
if self.filename.startswith(':/'):
return True
ret = QMessageBox.question(self, "Message",
"<h4><p>The document was modified.</p>\n" \
"<p>Do you want to save changes?</p></h4>",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
if ret == QMessageBox.Yes:
if self.filename == "":
self.fileSaveAs()
return False
else:
self.fileSave()
return True
if ret == QMessageBox.Cancel:
return False
return True
def findText(self):
ft = self.findfield.text()
if self.editor.find(ft):
return
else:
self.editor.moveCursor(1)
if self.editor.find(ft):
self.editor.moveCursor(QTextCursor.Start, QTextCursor.MoveAnchor)
def handleQuit(self):
print("Goodbye ...")
app.quit()
def set_numbers_visible(self, value = True):
self.numbers.setVisible(False)
def match_left(self, block, character, start, found):
map = {'{': '}', '(': ')', '[': ']'}
while block.isValid():
data = block.userData()
if data is not None:
braces = data.braces
N = len(braces)
for k in range(start, N):
if braces[k].character == character:
found += 1
if braces[k].character == map[character]:
if not found:
return braces[k].position + block.position()
else:
found -= 1
block = block.next()
start = 0
def match_right(self, block, character, start, found):
map = {'}': '{', ')': '(', ']': '['}
while block.isValid():
data = block.userData()
if data is not None:
braces = data.braces
if start is None:
start = len(braces)
for k in range(start - 1, -1, -1):
if braces[k].character == character:
found += 1
if braces[k].character == map[character]:
if found == 0:
return braces[k].position + block.position()
else:
found -= 1
block = block.previous()
start = None
# '''
cursor = self.editor.textCursor()
block = cursor.block()
data = block.userData()
previous, next = None, None
if data is not None:
position = cursor.position()
block_position = cursor.block().position()
braces = data.braces
N = len(braces)
for k in range(0, N):
if braces[k].position == position - block_position or braces[k].position == position - block_position - 1:
previous = braces[k].position + block_position
if braces[k].character in ['{', '(', '[']:
next = self.match_left(block,
braces[k].character,
k + 1, 0)
elif braces[k].character in ['}', ')', ']']:
next = self.match_right(block,
braces[k].character,
k, 0)
if next is None:
next = -1
if next is not None and next > 0:
if next == 0 and next >= 0:
format = QTextCharFormat()
cursor.setPosition(previous)
cursor.movePosition(QTextCursor.NextCharacter,
QTextCursor.KeepAnchor)
format.setBackground(QColor('white'))
self.left_selected_bracket.format = format
self.left_selected_bracket.cursor = cursor
cursor.setPosition(next)
cursor.movePosition(QTextCursor.NextCharacter,
QTextCursor.KeepAnchor)
format.setBackground(QColor('white'))
self.right_selected_bracket.format = format
self.right_selected_bracket.cursor = cursor
# '''
def paintEvent(self, event):
highlighted_line = QTextEdit.ExtraSelection()
highlighted_line.format.setBackground(lineHighlightColor)
highlighted_line.format.setProperty(QTextFormat
.FullWidthSelection,
QVariant(True))
highlighted_line.cursor = self.editor.textCursor()
highlighted_line.cursor.clearSelection()
self.editor.setExtraSelections([highlighted_line,
self.left_selected_bracket,
self.right_selected_bracket])
def document(self):
return self.editor.document
def isModified(self):
return self.editor.document().isModified()
def setModified(self, modified):
self.editor.document().setModified(modified)
def setLineWrapMode(self, mode):
self.editor.setLineWrapMode(mode)
def clear(self):
self.editor.clear()
def setPlainText(self, *args, **kwargs):
self.editor.setPlainText(*args, **kwargs)
def setDocumentTitle(self, *args, **kwargs):
self.editor.setDocumentTitle(*args, **kwargs)
def set_number_bar_visible(self, value):
self.numbers.setVisible(value)
def replaceAll(self):
print("replacing all")
oldtext = self.editor.document().toPlainText()
newtext = oldtext.replace(self.findfield.text(), self.replacefield.text())
self.editor.setPlainText(newtext)
self.setModified(True)
def replaceOne(self):
print("replacing all")
oldtext = self.editor.document().toPlainText()
newtext = oldtext.replace(self.findfield.text(), self.replacefield.text(), 1)
self.editor.setPlainText(newtext)
self.setModified(True)
def setCurrentFile(self, fileName):
self.curFile = fileName
if self.curFile:
self.setWindowTitle("%s - Recent Files" % self.strippedName(self.curFile))
else:
self.setWindowTitle("Recent Files")
settings = QSettings('Axel Schneider', 'PTEdit')
files = settings.value('recentFileList')
try:
files.remove(fileName)
except ValueError:
pass
files.insert(0, fileName)
del files[self.MaxRecentFiles:]
settings.setValue('recentFileList', files)
for widget in QApplication.topLevelWidgets():
if isinstance(widget, myEditor):
widget.updateRecentFileActions()
def updateRecentFileActions(self):
mytext = ""
settings = QSettings('Axel Schneider', 'PTEdit')
files = settings.value('recentFileList')
numRecentFiles = min(len(files), self.MaxRecentFiles)
for i in range(numRecentFiles):
text = "&%d %s" % (i + 1, self.strippedName(files[i]))
self.recentFileActs[i].setText(text)
self.recentFileActs[i].setData(files[i])
self.recentFileActs[i].setVisible(True)
for j in range(numRecentFiles, self.MaxRecentFiles):
self.recentFileActs[j].setVisible(False)
self.separatorAct.setVisible((numRecentFiles > 0))
def clearRecentFileList(self, fileName):
self.rmenu.clear()
def strippedName(self, fullFileName):
return QFileInfo(fullFileName).fileName()
def stylesheet2(self):
return """
QPlainTextEdit
{
background: #ECECEC;
color: #202020;
border: 1px solid #1EAE3D;
selection-background-color: #505050;
selection-color: #ACDED5;
}
QMenu
{
background: #F2F2F2;
color: #0E185F;
border: 1px solid #1EAE3D;
selection-background-color: #ACDED5;
}
"""
if __name__ == '__main__':
app = QApplication(sys.argv)
win = myEditor()
win.setWindowIcon(QIcon.fromTheme("application-text"))
win.setWindowTitle("Plain Text Edit" + "[*]")
win.setMinimumSize(640,250)
win.showMaximized()
if len(sys.argv) > 1:
print(sys.argv[1])
win.openFileOnStart(sys.argv[1])
app.exec_()