├── .gitignore ├── .idea ├── .gitignore ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── pyside_db_chart_mapping_example.iml └── vcs.xml ├── LICENSE ├── README.md ├── pyside_db_chart_mapping_example ├── __init__.py ├── chart │ ├── __init__.py │ ├── chart.py │ └── settings │ │ ├── __init__.py │ │ ├── colorButton.py │ │ ├── colorEditorWidget.py │ │ ├── colorHueBarWidget.py │ │ ├── colorPickerDialog.py │ │ ├── colorPickerWidget.py │ │ ├── colorSquareWidget.py │ │ └── settingsDialog.py ├── db │ ├── __init__.py │ ├── addColDialog.py │ ├── db.py │ └── delColDialog.py ├── file_sample │ ├── a.pdf │ ├── a.png │ └── a.xlsx ├── ico │ ├── __init__.py │ └── search.svg ├── main.py └── style │ ├── __init__.py │ ├── black_overlay.css │ ├── black_ring_of_color_selector.css │ ├── color_selector.css │ ├── hue_bg.css │ ├── hue_frame.css │ ├── hue_selector.css │ ├── lineedit.css │ ├── search_bar.css │ └── widget.css ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pyside_db_chart_mapping_example.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jung Gyu Yoon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyside-db-chart-mapping-example 2 | PySide6 example of mapping database table(QSqlTableModel based tableview) and chart with QVBarModelMapper. 3 | 4 | All basic CRUD feature of database mapped into chart(QChartView). 5 | 6 | You can save the chart as image/pdf file, you can see the saved file over here. 7 | 8 | You can find out more features and usages below. 9 | 10 | ## Requirements 11 | * PySide6 12 | 13 | ## Packages which will be automatically install (All of them are related to import/export as excel feature) 14 | * xlsxwriter - export as excel 15 | * pandas - import as excel 16 | * openpyxl - import as excel 17 | 18 | Note: pandas maybe requires more packages than above such as daas. 19 | 20 | ## Setup 21 | `python -m pip install git+https://github.com/yjg30737/pyside-db-chart-mapping-example.git --upgrade` 22 | 23 | ### If you don't want to import/export excel feature and install related libraries 24 | `python -m pip install git+https://github.com/yjg30737/pyside-db-chart-mapping-example.git@7d204961cd7462266ab15a20e9c0a62c40ab74fc` 25 | 26 | 27 | ## Usage/Feature 28 | * If you want to delete more than one record, holding ctrl and select records one by one or holding shift and select records as consecutive range. 29 | * If you change the data in table, chart data will be changed as well. Try changing name, score 1~3 fields or adding/deleting the record. It works like a charm. 30 | * 4 records are given by default to show how it works. 31 | * You can search the text in table with writing the text in search bar. Table will show the matched records, chart will be not influenced by search bar. 32 | * ID cell can't be editable which is supposed to be like that, you can write number only to score 1~3 columns. 33 | * You can save the chart as an image/pdf file. 34 | * If you put the mouse cursor on the bar, barset's border color will be changed. If you select/click one of the bar, its background color will be changed and text browser will show the bar's info. If cursor leaves, border color will be restored as normal. 35 | * You can change each color of the bar, choose to set the animation of chart in the settings dialog. 36 | * Import/export excel file 37 | * Being able to view the table info 38 | 39 | ## Example 40 | ```python 41 | from PySide6.QtWidgets import QApplication 42 | from pyside_db_chart_mapping_example.main import Window 43 | 44 | 45 | if __name__ == "__main__": 46 | import sys 47 | 48 | app = QApplication(sys.argv) 49 | window = Window() 50 | window.show() 51 | sys.exit(app.exec()) 52 | ``` 53 | 54 | Result 55 | 56 | ![python_example](https://user-images.githubusercontent.com/55078043/193983371-8fd5c1a3-db4e-45f1-b643-f2df71f9cb77.png) 57 | 58 | You don't have to care about left check box list. I'm still working on it. 59 | 60 | ![image](https://user-images.githubusercontent.com/55078043/193983236-7e5522fd-0cd9-42d7-93a1-f2266691bb51.png) 61 | 62 | If you place the mouse cursor on one of the bar, barset border's color will be changed as i mentioned before. In this case, border color turns to be red. 63 | 64 | Click the bar will change the bar's background color and show the bar's basic info on the text browser. In this case, background color turns to be green. 65 | 66 | ## See Also 67 | * BarModelMapper Example - table(not sql-based table) and chart mapping example in Qt documentation 68 | * pyside-database-chart-example - non-mapping version (i tried to map each other on my own, but failed) 69 | 70 | ## Note 71 | I'm struggling with the problem that item is not added more than one after table was empty. 72 | 73 | After much research i convince this is gotta be glitch. 74 | 75 | Don't want to report this to Qt however. Someone please do it for me. 76 | 77 | I just want to figure it out on my own. 78 | 79 | Another glitch i found is that you have to add more than one if you add 1 to the last column of the mapper(QVBarModelMapper). 80 | 81 | ```python 82 | self.__mapper.setLastBarSetColumn(self.__mapper.lastBarSetColumn()+2) 83 | ``` 84 | 85 | I don't even know what's going on here. 86 | -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/__init__.py: -------------------------------------------------------------------------------- 1 | from pyside_db_chart_mapping_example.main import Window 2 | -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjg30737/pyside-db-chart-mapping-example/176ee043aa7e031fa91531b591cb20fbd356bb7f/pyside_db_chart_mapping_example/chart/__init__.py -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/chart.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import typing 3 | 4 | from PySide6.QtCharts import QChart, QChartView, QBarSeries, QVBarModelMapper, \ 5 | QBarCategoryAxis, QValueAxis, QBarSet 6 | from PySide6.QtCore import Qt, QSettings 7 | from PySide6.QtGui import QPainter, QPixmap, QColor, QPdfWriter 8 | from PySide6.QtSql import QSqlQuery 9 | from PySide6.QtWidgets import QVBoxLayout, QWidget, QTextBrowser, QSplitter, QPushButton, QFileDialog, QHBoxLayout, \ 10 | QSpacerItem, QSizePolicy, QDialog 11 | 12 | from pyside_db_chart_mapping_example.chart.settings.settingsDialog import SettingsDialog 13 | from pyside_db_chart_mapping_example.db.db import SqlTableModel 14 | 15 | 16 | class ChartWidget(QWidget): 17 | def __init__(self): 18 | super().__init__() 19 | self.__initVal() 20 | self.__initUi() 21 | 22 | def __initVal(self): 23 | self.__idNameDict = {} 24 | self.__model = SqlTableModel() 25 | self.__initSettings() 26 | 27 | def __initSettings(self): 28 | self.__settingsStruct = QSettings('chart_settings.ini', QSettings.IniFormat) 29 | self.__animation = int(self.__settingsStruct.value('animation', 1)) 30 | self.__theme = self.__settingsStruct.value('theme', 'Light') 31 | self.__hoverColor = self.__settingsStruct.value('hoverColor', '#ff0000') 32 | self.__selectColor = self.__settingsStruct.value('selectColor', '#329b64') 33 | 34 | def __initUi(self): 35 | self.__chart = QChart() 36 | self.__setAnimation() 37 | self.__setTheme() 38 | 39 | self.__textBrowser = QTextBrowser() 40 | self.__textBrowser.setPlaceholderText('Place the mouse cursor over one of the bars to see the bar info here') 41 | 42 | self.__chartView = QChartView(self.__chart) 43 | self.__chartView.setRenderHint(QPainter.Antialiasing) 44 | 45 | mainWidget = QSplitter() 46 | mainWidget.addWidget(self.__chartView) 47 | mainWidget.addWidget(self.__textBrowser) 48 | mainWidget.setOrientation(Qt.Vertical) 49 | mainWidget.setHandleWidth(1) 50 | mainWidget.setStyleSheet( 51 | "QSplitterHandle {background-color: lightgray;}") 52 | mainWidget.setSizes([700, 300]) 53 | 54 | saveBtn = QPushButton('Save Chart') 55 | saveBtn.clicked.connect(self.__save) 56 | 57 | lay = QHBoxLayout() 58 | 59 | settingsBtn = QPushButton('Settings') 60 | settingsBtn.clicked.connect(self.__settings) 61 | 62 | lay.addWidget(settingsBtn) 63 | lay.addSpacerItem(QSpacerItem(10, 10, QSizePolicy.MinimumExpanding)) 64 | lay.addWidget(saveBtn) 65 | lay.setContentsMargins(5, 5, 5, 0) 66 | 67 | topWidget = QWidget() 68 | topWidget.setLayout(lay) 69 | 70 | lay = QVBoxLayout() 71 | lay.addWidget(topWidget) 72 | lay.addWidget(mainWidget) 73 | lay.setContentsMargins(0, 0, 0, 0) 74 | self.setLayout(lay) 75 | 76 | def mapDbModel(self, model: SqlTableModel): 77 | # set model and connect all events 78 | self.__model = model 79 | self.__model.added.connect(self.__addChartXCategory) 80 | self.__model.updated.connect(self.__updateChartXCategory) 81 | self.__model.deleted.connect(self.__removeChartXCategory) 82 | self.__model.addedCol.connect(self.__addColToSeries) 83 | self.__model.addedCol.connect(self.__removeBarSetColumn) 84 | 85 | # set mapper and series(bars on the chart) 86 | self.__series = QBarSeries() 87 | self.__series.barsetsAdded.connect(self.__setSelectedColor) 88 | 89 | self.__mapper = QVBarModelMapper(self) 90 | self.__mapper.setFirstBarSetColumn(4) 91 | self.__mapper.setLastBarSetColumn(6) 92 | self.__mapper.setFirstRow(0) 93 | self.__mapper.setRowCount(self.__model.rowCount()) 94 | self.__mapper.setSeries(self.__series) 95 | self.__mapper.setModel(self.__model) 96 | # self.__mapper.lastBarSetColumnChanged.connect(self.__addBarSetColumn) 97 | self.__chart.addSeries(self.__series) 98 | 99 | # get name attributes 100 | getNameQuery = QSqlQuery() 101 | getNameQuery.prepare(f'SELECT id, name FROM {self.__model.tableName()} order by ID') 102 | getNameQuery.exec() 103 | 104 | barsetLabelLst = [barset.label() for barset in self.__series.barSets()] 105 | 106 | # set name attributes to list widget 107 | # self.__barsetCheckListWidget.addItems(barsetLabelLst) 108 | 109 | # get name attributes 110 | nameLst = [] 111 | while getNameQuery.next(): 112 | name = getNameQuery.value('name') 113 | id = getNameQuery.value('id') 114 | self.__idNameDict[id] = name 115 | nameLst.append(name) 116 | 117 | # set name attributes to list widget 118 | # self.__axisCheckBoxListWidget.addItems(nameLst) 119 | 120 | # define axis X, set name attributes to it 121 | self.__axisX = QBarCategoryAxis() 122 | self.__axisX.append(nameLst) 123 | self.__chart.addAxis(self.__axisX, Qt.AlignBottom) 124 | self.__series.attachAxis(self.__axisX) 125 | 126 | # define axis Y 127 | self.__axisY = QValueAxis() 128 | self.__axisY.setTitleText('Score') 129 | self.__chart.addAxis(self.__axisY, Qt.AlignLeft) 130 | self.__series.attachAxis(self.__axisY) 131 | 132 | # set hover event to series 133 | self.__series.hovered.connect(self.__seriesHovered) 134 | self.__series.clicked.connect(self.__seriesPressed) 135 | 136 | def __addChartXCategory(self, id, name): 137 | self.__idNameDict[id] = name 138 | self.__axisX.append([name]) 139 | self.__mapper.setRowCount(self.__model.rowCount()) 140 | 141 | def __updateChartXCategory(self, id, newName): 142 | # get mapped name by id 143 | oldName = self.__idNameDict[id] 144 | self.__axisX.replace(oldName, newName) 145 | self.__idNameDict[id] = newName 146 | 147 | def __removeChartXCategory(self, names): 148 | # todo fix the bug 149 | # incorrect row count of chart 150 | # after removing the first row or consecutive rows including first 151 | # get id related to each name 152 | idLst = [] 153 | for id, name in self.__idNameDict.items(): 154 | if name in names: 155 | idLst.append(id) 156 | 157 | # delete the key/value pair in dictionary and category in chart 158 | for id in idLst: 159 | name = self.__idNameDict[id] 160 | self.__axisX.remove(name) 161 | del self.__idNameDict[id] 162 | self.__mapper.setRowCount(self.__model.rowCount()) 163 | 164 | def __addColToSeries(self): 165 | self.__mapper.setLastBarSetColumn(self.__mapper.lastBarSetColumn()+2) 166 | 167 | def __removeBarSetColumn(self): 168 | self.__mapper.setLastBarSetColumn(self.__mapper.lastBarSetColumn()-1) 169 | 170 | def __seriesHovered(self, status, idx, barset: QBarSet): 171 | if status: 172 | pen = barset.pen() 173 | pen.setColor(self.__hoverColor) 174 | barset.setPen(pen) 175 | else: 176 | pen = barset.pen() 177 | pen.setColor(QColor.fromHsvF(0.555833, 0.000000, 1.000000, 1.000000)) 178 | barset.setPen(pen) 179 | 180 | def __showSelectedBarInfo(self, idx, barset): 181 | barset.setBarSelected(idx, True) 182 | category = self.__axisX.categories()[idx] 183 | query = QSqlQuery() 184 | query.prepare(f"SELECT * FROM contacts WHERE name = \'{category}\'") 185 | query.exec() 186 | job = email = '' 187 | while query.next(): 188 | job = query.value('job') 189 | email = query.value('email') 190 | 191 | hoveredSeriesInfo = f''' 192 | Index of barset: {idx} 193 | Barset object: {barset} 194 | Barset object label: {barset.label()} 195 | Barset object category: {category} 196 | Barset object job: {job} 197 | Barset object email: {email} 198 | Barset object value: {barset.at(idx)} 199 | ''' 200 | self.__textBrowser.setText(hoveredSeriesInfo) 201 | 202 | def __seriesPressed(self, idx, barset): 203 | if barset.isBarSelected(idx): 204 | barset.setBarSelected(idx, False) 205 | self.__textBrowser.clear() 206 | else: 207 | for b in self.__series.barSets(): 208 | b.deselectAllBars() 209 | self.__textBrowser.clear() 210 | self.__showSelectedBarInfo(idx, barset) 211 | 212 | def __setAnimation(self): 213 | if self.__animation: 214 | self.__chart.setAnimationOptions(QChart.AllAnimations) 215 | else: 216 | self.__chart.setAnimationOptions(QChart.NoAnimation) 217 | 218 | def __setTheme(self): 219 | if self.__theme == 'Light': 220 | self.__chart.setTheme(QChart.ChartThemeLight) 221 | else: 222 | self.__chart.setTheme(QChart.ChartThemeDark) 223 | 224 | def __setSelectedColor(self, barsets: typing.Iterable[QBarSet]): 225 | for barset in barsets: 226 | barset.setSelectedColor(self.__selectColor) 227 | 228 | def __settings(self): 229 | dialog = SettingsDialog() 230 | reply = dialog.exec() 231 | if reply == QDialog.Accepted: 232 | self.__initSettings() 233 | self.__setAnimation() 234 | self.__setTheme() 235 | self.__setSelectedColor(self.__series.barSets()) 236 | 237 | def __save(self): 238 | filename = QFileDialog.getSaveFileName(self, 'Save', '.', 'PNG (*.png);; ' 239 | 'JPEG (*.jpg;*.jpeg);;' 240 | 'PDF (*.pdf)') 241 | ext = filename[1].split('(')[0].strip() 242 | filename = filename[0] 243 | if filename: 244 | # pdf file 245 | if ext == 'PDF': 246 | writer = QPdfWriter(filename) 247 | writer.setResolution(100) 248 | p = QPainter() 249 | p.begin(writer) 250 | self.__chartView.render(p) 251 | p.setRenderHint(QPainter.SmoothPixmapTransform) 252 | p.end() 253 | # image file 254 | else: 255 | dpr = self.__chartView.devicePixelRatioF() 256 | # dpr, *2 is for high quality image 257 | pixmap = QPixmap(int(self.__chartView.width() * dpr * 2), 258 | int(self.__chartView.height() * dpr * 2)) 259 | # make the background transparent 260 | pixmap.fill(Qt.transparent) 261 | p = QPainter(pixmap) 262 | p.setRenderHint(QPainter.Antialiasing) 263 | p.begin(pixmap) 264 | self.__chartView.render(p) 265 | p.end() 266 | pixmap.save(filename, ext) 267 | 268 | path = filename.replace('/', '\\') 269 | subprocess.Popen(r'explorer /select,"' + path + '"') 270 | 271 | def getBarsetsTextList(self): 272 | return [barset.label() for barset in self.__series.barSets()] 273 | 274 | def getCategories(self): 275 | return self.__axisX.categories() -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjg30737/pyside-db-chart-mapping-example/176ee043aa7e031fa91531b591cb20fbd356bb7f/pyside_db_chart_mapping_example/chart/settings/__init__.py -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/settings/colorButton.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Signal 2 | from PySide6.QtGui import QColor 3 | from PySide6.QtWidgets import QPushButton 4 | 5 | 6 | class ColorButton(QPushButton): 7 | colorChanged = Signal(QColor) 8 | 9 | def __init__(self, color, size=20): 10 | super().__init__() 11 | self.__initVal(color, size) 12 | self.__initUi() 13 | 14 | def __initVal(self, color, size): 15 | self.__color = QColor(color) 16 | self.__size = size 17 | 18 | def __initUi(self): 19 | self.setFixedSize(self.__size, self.__size) 20 | self.__initStyle() 21 | 22 | def setColor(self, rgb): 23 | if isinstance(rgb, tuple): 24 | r = int(rgb[0]) 25 | g = int(rgb[1]) 26 | b = int(rgb[2]) 27 | self.__color = QColor(r, g, b) 28 | elif isinstance(rgb, QColor): 29 | self.__color = rgb 30 | self.__initStyle() 31 | self.colorChanged.emit(self.__color) 32 | 33 | def getColor(self): 34 | return self.__color 35 | 36 | def __initStyle(self): 37 | self.setStyleSheet(f''' 38 | QPushButton 39 | {{ 40 | border-width:1px; 41 | border-radius: {str(self.__size//2)}; 42 | background-color: {self.__color.name()}; 43 | }} 44 | ''' 45 | ) -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/settings/colorEditorWidget.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Qt, Signal 2 | from PySide6.QtGui import QColor, QFont 3 | from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QSpinBox, QLineEdit 4 | 5 | 6 | class ColorEditorWidget(QWidget): 7 | colorChanged = Signal(QColor) 8 | 9 | def __init__(self, color, orientation): 10 | super().__init__() 11 | self.__current_color = color 12 | self.__initUi(color, orientation) 13 | 14 | def __initUi(self, color, orientation): 15 | self.__colorPreviewWithGraphics = QWidget() 16 | self.__colorPreviewWithGraphics.setFixedWidth(200) 17 | self.__colorPreviewWithGraphics.setMinimumHeight(75) 18 | self.setColorPreviewWithGraphics() 19 | 20 | self.__hLineEdit = QLineEdit() 21 | self.__hLineEdit.setReadOnly(True) 22 | 23 | self.__rSpinBox = QSpinBox() 24 | self.__gSpinBox = QSpinBox() 25 | self.__bSpinBox = QSpinBox() 26 | 27 | self.__rSpinBox.valueChanged.connect(self.__rColorChanged) 28 | self.__gSpinBox.valueChanged.connect(self.__gColorChanged) 29 | self.__bSpinBox.valueChanged.connect(self.__bColorChanged) 30 | 31 | self.__hLineEdit.setAlignment(Qt.AlignCenter) 32 | self.__hLineEdit.setFont(QFont('Arial', 12)) 33 | 34 | spinBoxs = [self.__rSpinBox, self.__gSpinBox, self.__bSpinBox] 35 | for spinBox in spinBoxs: 36 | spinBox.setRange(0, 255) 37 | spinBox.setAlignment(Qt.AlignCenter) 38 | spinBox.setFont(QFont('Arial', 12)) 39 | 40 | lay = QFormLayout() 41 | lay.addRow('#', self.__hLineEdit) 42 | lay.addRow('R', self.__rSpinBox) 43 | lay.addRow('G', self.__gSpinBox) 44 | lay.addRow('B', self.__bSpinBox) 45 | lay.setContentsMargins(0, 0, 0, 0) 46 | 47 | colorEditor = QWidget() 48 | colorEditor.setLayout(lay) 49 | if orientation == 'horizontal': 50 | lay = QVBoxLayout() 51 | elif orientation == 'vertical': 52 | lay = QHBoxLayout() 53 | lay.addWidget(self.__colorPreviewWithGraphics) 54 | lay.addWidget(colorEditor) 55 | 56 | lay.setContentsMargins(0, 0, 0, 0) 57 | 58 | self.setLayout(lay) 59 | 60 | self.setColor(color) 61 | 62 | def setColorPreviewWithGraphics(self): 63 | self.__colorPreviewWithGraphics.setStyleSheet(f' border-radius: 5px; ' 64 | f'background-color: {self.__current_color.name()}; ') 65 | 66 | def setColor(self, color): 67 | self.__current_color = color 68 | self.setColorPreviewWithGraphics() 69 | self.__hLineEdit.setText(self.__current_color.name()) 70 | 71 | # Prevent infinite valueChanged event loop 72 | self.__rSpinBox.valueChanged.disconnect(self.__rColorChanged) 73 | self.__gSpinBox.valueChanged.disconnect(self.__gColorChanged) 74 | self.__bSpinBox.valueChanged.disconnect(self.__bColorChanged) 75 | 76 | r, g, b = self.__current_color.red(), self.__current_color.green(), self.__current_color.blue() 77 | 78 | self.__rSpinBox.setValue(r) 79 | self.__gSpinBox.setValue(g) 80 | self.__bSpinBox.setValue(b) 81 | 82 | self.__rSpinBox.valueChanged.connect(self.__rColorChanged) 83 | self.__gSpinBox.valueChanged.connect(self.__gColorChanged) 84 | self.__bSpinBox.valueChanged.connect(self.__bColorChanged) 85 | 86 | def __rColorChanged(self, r): 87 | self.__current_color.setRed(r) 88 | self.__procColorChanged() 89 | 90 | def __gColorChanged(self, g): 91 | self.__current_color.setGreen(g) 92 | self.__procColorChanged() 93 | 94 | def __bColorChanged(self, b): 95 | self.__current_color.setBlue(b) 96 | self.__procColorChanged() 97 | 98 | def __procColorChanged(self): 99 | self.__hLineEdit.setText(self.__current_color.name()) 100 | self.setColorPreviewWithGraphics() 101 | self.colorChanged.emit(self.__current_color) 102 | 103 | def getCurrentColor(self): 104 | return self.__current_color -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/settings/colorHueBarWidget.py: -------------------------------------------------------------------------------- 1 | import math, colorsys, os 2 | 3 | from PySide6.QtCore import QPoint, Qt, Signal 4 | from PySide6.QtWidgets import QWidget, QLabel 5 | 6 | 7 | class ColorHueBarWidget(QWidget): 8 | hueChanged = Signal(int) 9 | hueChangedByEditor = Signal(int) 10 | 11 | def __init__(self, color): 12 | super().__init__() 13 | self.__initUi(color) 14 | 15 | def __initUi(self, color): 16 | self.__hue_bar_height = 300 17 | self.__hue_bar_width = 20 18 | self.setFixedSize(self.__hue_bar_width, self.__hue_bar_height) 19 | 20 | self.__hue_selector_height = 15 21 | self.__hue_selector_moving_range = self.__hue_bar_height-self.__hue_selector_height 22 | 23 | hueFrame = QWidget(self) 24 | with open(os.path.join(os.path.dirname(__file__), '../../style/hue_frame.css'), 'r') as f: 25 | hueFrame.setStyleSheet(f.read()) 26 | 27 | hueBg = QWidget(hueFrame) 28 | hueBg.setFixedWidth(self.__hue_bar_width) 29 | hueBg.setMinimumHeight(self.__hue_bar_height) 30 | with open(os.path.join(os.path.dirname(__file__), '../../style/hue_bg.css'), 'r') as f: 31 | hueBg.setStyleSheet(f.read()) 32 | 33 | self.__hue_selector = QLabel(hueFrame) 34 | self.__hue_selector.setGeometry(0, 0, self.__hue_bar_width, self.__hue_selector_height) 35 | self.__hue_selector.setMinimumSize(self.__hue_bar_width, 0) 36 | with open(os.path.join(os.path.dirname(__file__), '../../style/hue_selector.css'), 'r') as f: 37 | self.__hue_selector.setStyleSheet(f.read()) 38 | 39 | hueFrame.mouseMoveEvent = self.__moveSelectorByCursor 40 | hueFrame.mousePressEvent = self.__moveSelectorByCursor 41 | 42 | h, s, v = colorsys.rgb_to_hsv(color.redF(), color.greenF(), color.blueF()) 43 | self.__initHueSelector(h) 44 | 45 | def __moveSelectorByCursor(self, e): 46 | if e.buttons() == Qt.LeftButton: 47 | pos = e.pos().y() - math.floor(self.__hue_selector_height/2) 48 | if pos < 0: 49 | pos = 0 50 | if pos > self.__hue_selector_moving_range: 51 | pos = self.__hue_selector_moving_range 52 | self.__hue_selector.move(QPoint(0, pos)) 53 | 54 | h = self.__hue_selector.y() / self.__hue_selector_moving_range * 100 55 | self.hueChanged.emit(h) 56 | 57 | def __moveSelectorNotByCursor(self, h): 58 | geo = self.__hue_selector.geometry() 59 | 60 | # Prevent y from becoming larger than minimumHeight 61 | # if y becomes larger than minimumHeight, selector will be placed out of the bottom boundary. 62 | y = min(self.__hue_selector_moving_range, h * self.minimumHeight()) 63 | geo.moveTo(0, y) 64 | self.__hue_selector.setGeometry(geo) 65 | 66 | h = self.__hue_selector.y() / self.__hue_selector_moving_range * 100 67 | self.hueChangedByEditor.emit(h) 68 | 69 | def __initHueSelector(self, h): 70 | self.__moveSelectorNotByCursor(h) 71 | 72 | def moveSelectorByEditor(self, h): 73 | self.__moveSelectorNotByCursor(h) 74 | -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/settings/colorPickerDialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtGui import QColor 2 | from PySide6.QtWidgets import QDialog, QHBoxLayout, QPushButton, QWidget, QVBoxLayout, QFrame 3 | from PySide6.QtCore import Qt 4 | 5 | from pyside_db_chart_mapping_example.chart.settings.colorPickerWidget import ColorPickerWidget 6 | 7 | 8 | class ColorPickerDialog(QDialog): 9 | def __init__(self, color=QColor(255, 255, 255), orientation='horizontal'): 10 | super().__init__() 11 | if isinstance(color, QColor): 12 | pass 13 | elif isinstance(color, str): 14 | color = QColor(color) 15 | self.__initUi(color=color, orientation=orientation) 16 | 17 | def __initUi(self, color, orientation): 18 | self.setWindowFlags(Qt.WindowCloseButtonHint | Qt.MSWindowsFixedSizeDialogHint) 19 | self.setWindowTitle('Pick the color') 20 | 21 | self.__colorPickerWidget = ColorPickerWidget(color, orientation) 22 | 23 | okBtn = QPushButton('OK') 24 | cancelBtn = QPushButton('Cancel') 25 | 26 | okBtn.clicked.connect(self.accept) 27 | cancelBtn.clicked.connect(self.close) 28 | 29 | if orientation == 'horizontal': 30 | lay = QHBoxLayout() 31 | lay.addWidget(self.__colorPickerWidget) 32 | lay.setContentsMargins(0, 0, 0, 0) 33 | 34 | topWidget = QWidget() 35 | topWidget.setLayout(lay) 36 | 37 | lay = QHBoxLayout() 38 | lay.setAlignment(Qt.AlignRight) 39 | lay.addWidget(okBtn) 40 | lay.addWidget(cancelBtn) 41 | lay.setContentsMargins(0, 0, 0, 0) 42 | 43 | bottomWidget = QWidget() 44 | bottomWidget.setLayout(lay) 45 | 46 | sep = QFrame() 47 | sep.setFrameShape(QFrame.HLine) 48 | sep.setFrameShadow(QFrame.Sunken) 49 | sep.setContentsMargins(0, 0, 0, 0) 50 | 51 | lay = QVBoxLayout() 52 | lay.addWidget(topWidget) 53 | lay.addWidget(sep) 54 | lay.addWidget(bottomWidget) 55 | elif orientation == 'vertical': 56 | lay = QHBoxLayout() 57 | lay.addWidget(self.__colorPickerWidget) 58 | lay.setContentsMargins(0, 0, 0, 0) 59 | 60 | leftWidget = QWidget() 61 | leftWidget.setLayout(lay) 62 | 63 | lay = QVBoxLayout() 64 | lay.setAlignment(Qt.AlignBottom) 65 | lay.addWidget(okBtn) 66 | lay.addWidget(cancelBtn) 67 | lay.setContentsMargins(0, 0, 0, 0) 68 | 69 | rightWidget = QWidget() 70 | rightWidget.setLayout(lay) 71 | 72 | sep = QFrame() 73 | sep.setFrameShape(QFrame.VLine) 74 | sep.setFrameShadow(QFrame.Sunken) 75 | sep.setContentsMargins(0, 0, 0, 0) 76 | 77 | lay = QHBoxLayout() 78 | lay.addWidget(leftWidget) 79 | lay.addWidget(sep) 80 | lay.addWidget(rightWidget) 81 | 82 | self.setLayout(lay) 83 | 84 | def accept(self) -> None: 85 | return super().accept() 86 | 87 | def getColor(self) -> QColor: 88 | return self.__colorPickerWidget.getCurrentColor() -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/settings/colorPickerWidget.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | 3 | from PySide6.QtGui import QColor 4 | from PySide6.QtCore import Qt, Signal 5 | from PySide6.QtWidgets import QWidget, QHBoxLayout, QGridLayout 6 | 7 | from pyside_db_chart_mapping_example.chart.settings.colorEditorWidget import ColorEditorWidget 8 | from pyside_db_chart_mapping_example.chart.settings.colorHueBarWidget import ColorHueBarWidget 9 | from pyside_db_chart_mapping_example.chart.settings.colorSquareWidget import ColorSquareWidget 10 | 11 | 12 | class ColorPickerWidget(QWidget): 13 | colorChanged = Signal(QColor) 14 | 15 | def __init__(self, color=QColor(255, 255, 255), orientation='horizontal'): 16 | super().__init__() 17 | if isinstance(color, QColor): 18 | pass 19 | elif isinstance(color, str): 20 | color = QColor(color) 21 | self.__initUi(color=color, orientation=orientation) 22 | 23 | def __initUi(self, color, orientation): 24 | self.__colorSquareWidget = ColorSquareWidget(color) 25 | self.__colorSquareWidget.colorChanged.connect(self.__colorChanged) 26 | 27 | self.__colorHueBarWidget = ColorHueBarWidget(color) 28 | self.__colorHueBarWidget.hueChanged.connect(self.__hueChanged) 29 | self.__colorHueBarWidget.hueChangedByEditor.connect(self.__hueChangedByEditor) 30 | 31 | self.__colorEditorWidget = ColorEditorWidget(color, orientation=orientation) 32 | self.__colorEditorWidget.colorChanged.connect(self.__colorChangedByEditor) 33 | 34 | if orientation == 'horizontal': 35 | lay = QHBoxLayout() 36 | lay.addWidget(self.__colorSquareWidget) 37 | lay.addWidget(self.__colorHueBarWidget) 38 | lay.addWidget(self.__colorEditorWidget) 39 | elif orientation == 'vertical': 40 | lay = QGridLayout() 41 | lay.addWidget(self.__colorSquareWidget, 0, 0, 1, 1) 42 | lay.addWidget(self.__colorHueBarWidget, 0, 1, 1, 1) 43 | lay.addWidget(self.__colorEditorWidget, 1, 0, 1, 2) 44 | lay.setAlignment(Qt.AlignTop) 45 | 46 | mainWidget = QWidget() 47 | mainWidget.setLayout(lay) 48 | lay.setContentsMargins(0, 0, 0, 0) 49 | 50 | self.setLayout(lay) 51 | 52 | def __hueChanged(self, h): 53 | self.__colorSquareWidget.changeHue(h) 54 | 55 | def __hueChangedByEditor(self, h): 56 | self.__colorSquareWidget.changeHueByEditor(h) 57 | 58 | def hsv2rgb(self, h, s, v): 59 | return tuple(round(i * 255) for i in colorsys.hsv_to_rgb(h, s, v)) 60 | 61 | def __colorChanged(self, h, s, l): 62 | r, g, b = self.hsv2rgb(h / 100, s, l) 63 | color = QColor(r, g, b) 64 | self.__colorEditorWidget.setColor(color) 65 | self.colorChanged.emit(color) 66 | 67 | def __colorChangedByEditor(self, color: QColor): 68 | h, s, v = colorsys.rgb_to_hsv(color.redF(), color.greenF(), color.blueF()) 69 | self.__colorHueBarWidget.moveSelectorByEditor(h) 70 | self.__colorSquareWidget.moveSelectorByEditor(s, v) 71 | self.colorChanged.emit(color) 72 | 73 | def getCurrentColor(self): 74 | return self.__colorEditorWidget.getCurrentColor() -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/settings/colorSquareWidget.py: -------------------------------------------------------------------------------- 1 | import math, colorsys, os 2 | 3 | from PySide6.QtWidgets import QWidget, QGridLayout, QLabel 4 | 5 | from PySide6.QtCore import Qt, QPoint, Signal, QRect 6 | 7 | 8 | class ColorSquareWidget(QWidget): 9 | colorChanged = Signal(float, float, float) 10 | 11 | def __init__(self, color): 12 | super().__init__() 13 | self.__initUi(color) 14 | 15 | def __initUi(self, color): 16 | self.setFixedSize(300, 300) 17 | 18 | self.__h, \ 19 | self.__s, \ 20 | self.__l = colorsys.rgb_to_hsv(color.redF(), color.greenF(), color.blueF()) 21 | 22 | # Multiply 100 for insert into stylesheet code 23 | self.__h *= 100 24 | 25 | self.__colorView = QWidget() 26 | self.__colorView.setStyleSheet(f''' 27 | background-color: qlineargradient(x1:1, x2:0, 28 | stop:0 hsl({self.__h}%,100%,50%), 29 | stop:1 #fff); 30 | border-radius: 5px; 31 | ''') 32 | 33 | self.__blackOverlay = QWidget() 34 | with open(os.path.join(os.path.dirname(__file__), '../../style/black_overlay.css'), 'r') as f: 35 | self.__blackOverlay.setStyleSheet(f.read()) 36 | 37 | self.__blackOverlay.mouseMoveEvent = self.__moveSelectorByCursor 38 | self.__blackOverlay.mousePressEvent = self.__moveSelectorByCursor 39 | 40 | self.__selector_diameter = 12 41 | 42 | self.__selector = QWidget(self.__blackOverlay) 43 | self.__selector.setGeometry(math.floor(self.__selector_diameter / 2) * -1, 44 | math.floor(self.__selector_diameter / 2) * -1, 45 | self.__selector_diameter, 46 | self.__selector_diameter) 47 | with open(os.path.join(os.path.dirname(__file__), '../../style/color_selector.css'), 'r') as f: 48 | self.__selector.setStyleSheet(f.read()) 49 | 50 | self.__blackRingInsideSelector = QLabel(self.__selector) 51 | self.__blackRingInsideSelector_diameter = self.__selector_diameter - 2 52 | self.__blackRingInsideSelector.setGeometry(QRect(1, 1, self.__blackRingInsideSelector_diameter, 53 | self.__blackRingInsideSelector_diameter)) 54 | with open(os.path.join(os.path.dirname(__file__), '../../style/black_ring_of_color_selector.css'), 'r') as f: 55 | self.__blackRingInsideSelector.setStyleSheet(f.read()) 56 | 57 | lay = QGridLayout() 58 | lay.addWidget(self.__colorView, 0, 0, 1, 1) 59 | lay.addWidget(self.__blackOverlay, 0, 0, 1, 1) 60 | lay.setContentsMargins(0, 0, 0, 0) 61 | 62 | self.setLayout(lay) 63 | 64 | self.__initSelector() 65 | 66 | def __moveSelectorNotByCursor(self, s, l): 67 | geo = self.__selector.geometry() 68 | x = self.minimumWidth() * s 69 | y = self.minimumHeight() - self.minimumHeight() * l 70 | geo.moveCenter(QPoint(x, y)) 71 | self.__selector.setGeometry(geo) 72 | 73 | def __initSelector(self): 74 | self.__moveSelectorNotByCursor(self.__s, self.__l) 75 | 76 | def __moveSelectorByCursor(self, e): 77 | if e.buttons() == Qt.LeftButton: 78 | pos = e.pos() 79 | if pos.x() < 0: 80 | pos.setX(0) 81 | if pos.y() < 0: 82 | pos.setY(0) 83 | if pos.x() > 300: 84 | pos.setX(300) 85 | if pos.y() > 300: 86 | pos.setY(300) 87 | 88 | self.__selector.move(pos - QPoint(math.floor(self.__selector_diameter / 2), 89 | math.floor(self.__selector_diameter / 2))) 90 | 91 | self.__setSaturation() 92 | self.__setLightness() 93 | 94 | self.colorChanged.emit(self.__h, self.__s, self.__l) 95 | 96 | def changeHue(self, h): 97 | self.__h = h 98 | self.__colorView.setStyleSheet(f''' 99 | border-radius: 5px; 100 | background-color: qlineargradient(x1:1, x2:0, 101 | stop:0 hsl({self.__h}%,100%,50%), 102 | stop:1 #fff); 103 | ''') 104 | 105 | self.colorChanged.emit(self.__h, self.__s, self.__l) 106 | 107 | def changeHueByEditor(self, h): 108 | # Prevent hue from becoming larger than 100 109 | # if hue becomes larger than 100, hue of square will turn into dark. 110 | self.__h = min(100, h) 111 | self.__colorView.setStyleSheet(f''' 112 | border-radius: 5px; 113 | background-color: qlineargradient(x1:1, x2:0, 114 | stop:0 hsl({self.__h}%,100%,50%), 115 | stop:1 #fff); 116 | ''') 117 | 118 | def __setSaturation(self): 119 | self.__s = (self.__selector.pos().x() + math.floor(self.__selector_diameter / 2)) / self.minimumWidth() 120 | 121 | def getSaturatation(self): 122 | return self.__s 123 | 124 | def __setLightness(self): 125 | self.__l = abs( 126 | ((self.__selector.pos().y() + math.floor(self.__selector_diameter / 2)) / self.minimumHeight()) - 1) 127 | 128 | def getLightness(self): 129 | return self.__l 130 | 131 | def moveSelectorByEditor(self, s, l): 132 | self.__moveSelectorNotByCursor(s, l) -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/chart/settings/settingsDialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QSettings, Qt 2 | from PySide6.QtWidgets import QDialog, QWidget, QVBoxLayout, QGroupBox, QFormLayout, QPushButton, QHBoxLayout, \ 3 | QCheckBox, QGridLayout, QLabel, QComboBox 4 | 5 | from pyside_db_chart_mapping_example.chart.settings.colorButton import ColorButton 6 | from pyside_db_chart_mapping_example.chart.settings.colorPickerDialog import ColorPickerDialog 7 | 8 | 9 | class SettingsDialog(QDialog): 10 | def __init__(self): 11 | super().__init__() 12 | self.__initVal() 13 | self.__initUi() 14 | 15 | def __initVal(self): 16 | self.__settingsStruct = QSettings('chart_settings.ini', QSettings.IniFormat) 17 | self.__animation = int(self.__settingsStruct.value('animation', 1)) 18 | self.__theme = self.__settingsStruct.value('theme', 'Light') 19 | self.__hoverColor = self.__settingsStruct.value('hoverColor', '#ff0000') 20 | self.__selectColor = self.__settingsStruct.value('selectColor', '#329b64') 21 | 22 | def __initUi(self): 23 | self.setWindowTitle('Chart Settings') 24 | 25 | self.__hoverColorBtn = ColorButton(self.__hoverColor) 26 | self.__selectColorBtn = ColorButton(self.__selectColor) 27 | 28 | self.__hoverColorBtn.clicked.connect(self.__setHoverColor) 29 | self.__selectColorBtn.clicked.connect(self.__setSelectColor) 30 | 31 | animationChkBox = QCheckBox('Animation') 32 | animationChkBox.setChecked(bool(self.__animation)) 33 | animationChkBox.toggled.connect(self.__animationToggle) 34 | 35 | self.__themeCmbBox = QComboBox() 36 | self.__themeCmbBox.addItems(['Light', 'Dark']) 37 | self.__themeCmbBox.setCurrentText(self.__theme) 38 | self.__themeCmbBox.currentTextChanged.connect(self.__themeChanged) 39 | 40 | lay = QGridLayout() 41 | lay.addWidget(animationChkBox, 0, 0, 1, 1) 42 | lay.addWidget(QLabel('Bar border\'s color when cursor is hovering on it'), 1, 0, 1, 1) 43 | lay.addWidget(self.__hoverColorBtn, 1, 1, 1, 1, Qt.AlignRight) 44 | lay.addWidget(QLabel('Selected bar\'s color'), 2, 0, 1, 1) 45 | lay.addWidget(self.__selectColorBtn, 2, 1, 1, 1, Qt.AlignRight) 46 | lay.addWidget(QLabel('Theme'), 3, 0, 1, 1) 47 | lay.addWidget(self.__themeCmbBox, 3, 1, 1, 1, Qt.AlignRight) 48 | 49 | settingsGrpBox = QGroupBox() 50 | settingsGrpBox.setTitle('Chart Settings') 51 | settingsGrpBox.setLayout(lay) 52 | 53 | lay = QVBoxLayout() 54 | lay.addWidget(settingsGrpBox) 55 | 56 | topWidget = QWidget() 57 | topWidget.setLayout(lay) 58 | 59 | okBtn = QPushButton('OK') 60 | okBtn.clicked.connect(self.accept) 61 | 62 | closeBtn = QPushButton('Close') 63 | closeBtn.clicked.connect(self.close) 64 | 65 | lay = QHBoxLayout() 66 | lay.addWidget(okBtn) 67 | lay.addWidget(closeBtn) 68 | lay.setContentsMargins(0, 0, 0, 0) 69 | 70 | bottomWidget = QWidget() 71 | bottomWidget.setLayout(lay) 72 | 73 | lay = QVBoxLayout() 74 | lay.addWidget(topWidget) 75 | lay.addWidget(bottomWidget) 76 | 77 | self.setLayout(lay) 78 | 79 | self.setFixedSize(self.sizeHint().width(), self.sizeHint().height()) 80 | 81 | def __setHoverColor(self): 82 | dialog = ColorPickerDialog(self.__hoverColorBtn.getColor()) 83 | reply = dialog.exec() 84 | if reply == QDialog.Accepted: 85 | newColor = dialog.getColor() 86 | self.__hoverColorBtn.setColor(newColor) 87 | self.__settingsStruct.setValue('hoverColor', newColor.name()) 88 | 89 | def __setSelectColor(self): 90 | dialog = ColorPickerDialog(self.__selectColorBtn.getColor()) 91 | reply = dialog.exec() 92 | if reply == QDialog.Accepted: 93 | newColor = dialog.getColor() 94 | self.__selectColorBtn.setColor(newColor) 95 | self.__settingsStruct.setValue('selectColor', newColor.name()) 96 | 97 | def __animationToggle(self, f): 98 | self.__animation = int(f) 99 | self.__settingsStruct.setValue('animation', self.__animation) 100 | 101 | def __themeChanged(self, text): 102 | self.__theme = text 103 | self.__settingsStruct.setValue('theme', self.__theme) 104 | 105 | def getAnimation(self): 106 | return self.__animation 107 | 108 | def getHoverColor(self): 109 | return self.__hoverColorBtn.getColor() 110 | 111 | def getSelectColor(self): 112 | return self.__selectColorBtn.getColor() -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjg30737/pyside-db-chart-mapping-example/176ee043aa7e031fa91531b591cb20fbd356bb7f/pyside_db_chart_mapping_example/db/__init__.py -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/db/addColDialog.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from PySide6.QtWidgets import QDialog, QVBoxLayout, QWidget, QPushButton, QHBoxLayout, QFormLayout, \ 4 | QLineEdit 5 | 6 | 7 | class AddColDialog(QDialog): 8 | def __init__(self): 9 | super().__init__() 10 | self.__initUi() 11 | 12 | def __initUi(self): 13 | self.setWindowTitle('Add Column') 14 | 15 | self.__colNameLineEdit = QLineEdit() 16 | self.__colNameLineEdit.textChanged.connect(self.__checkAccept) 17 | 18 | lay = QFormLayout() 19 | lay.addRow('Name', self.__colNameLineEdit) 20 | 21 | topWidget = QWidget() 22 | topWidget.setLayout(lay) 23 | 24 | self.__okBtn = QPushButton('OK') 25 | self.__okBtn.clicked.connect(self.accept) 26 | self.__okBtn.setEnabled(False) 27 | 28 | closeBtn = QPushButton('Close') 29 | closeBtn.clicked.connect(self.close) 30 | 31 | lay = QHBoxLayout() 32 | lay.addWidget(self.__okBtn) 33 | lay.addWidget(closeBtn) 34 | lay.setContentsMargins(0, 0, 0, 0) 35 | 36 | bottomWidget = QWidget() 37 | bottomWidget.setLayout(lay) 38 | 39 | lay = QVBoxLayout() 40 | lay.addWidget(topWidget) 41 | lay.addWidget(bottomWidget) 42 | 43 | self.setLayout(lay) 44 | 45 | self.setFixedSize(self.sizeHint().width(), self.sizeHint().height()) 46 | 47 | def __checkAccept(self, text): 48 | p = bool(re.match('^[a-zA-Z0-9]+(\s*[a-zA-Z0-9])+', text)) 49 | self.__okBtn.setEnabled(p) 50 | 51 | def getColumnName(self): 52 | return self.__colNameLineEdit.text() -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/db/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3, xlsxwriter 3 | 4 | import PySide6 5 | import pandas as pd 6 | from typing import Union 7 | import subprocess 8 | 9 | from PySide6.QtGui import QIntValidator 10 | from PySide6.QtSql import QSqlTableModel, QSqlQuery, QSqlDatabase 11 | from PySide6.QtSvgWidgets import QSvgWidget 12 | from PySide6.QtWidgets import QTableView, QWidget, QHBoxLayout, QApplication, QLabel, QAbstractItemView, \ 13 | QGridLayout, QLineEdit, QMessageBox, QStyledItemDelegate, QPushButton, QComboBox, QSpacerItem, QSizePolicy, \ 14 | QVBoxLayout, QDialog, QFileDialog, QTableWidget, QTableWidgetItem 15 | from PySide6.QtCore import Qt, Signal, QSortFilterProxyModel, QModelIndex, QPersistentModelIndex, \ 16 | QAbstractTableModel 17 | 18 | from pyside_db_chart_mapping_example.db.addColDialog import AddColDialog 19 | from pyside_db_chart_mapping_example.db.delColDialog import DelColDialog 20 | 21 | 22 | class InstantSearchBar(QWidget): 23 | searched = Signal(str) 24 | 25 | def __init__(self, parent=None): 26 | super().__init__(parent) 27 | self.__initUi() 28 | 29 | def __initUi(self): 30 | # search bar label 31 | self.__label = QLabel() 32 | 33 | self.__searchLineEdit = QLineEdit() 34 | self.__searchIcon = QSvgWidget() 35 | ps = QApplication.font().pointSize() 36 | self.__searchIcon.setFixedSize(ps, ps) 37 | 38 | self.__searchBar = QWidget() 39 | self.__searchBar.setObjectName('searchBar') 40 | 41 | lay = QHBoxLayout() 42 | lay.addWidget(self.__searchIcon) 43 | lay.addWidget(self.__searchLineEdit) 44 | self.__searchBar.setLayout(lay) 45 | lay.setContentsMargins(ps // 2, 0, 0, 0) 46 | lay.setSpacing(0) 47 | 48 | self.__searchLineEdit.setFocus() 49 | self.__searchLineEdit.textChanged.connect(self.__searched) 50 | 51 | self.setAutoFillBackground(True) 52 | 53 | lay = QHBoxLayout() 54 | lay.addWidget(self.__searchBar) 55 | lay.setContentsMargins(0, 0, 0, 0) 56 | lay.setSpacing(2) 57 | 58 | self._topWidget = QWidget() 59 | self._topWidget.setLayout(lay) 60 | 61 | lay = QGridLayout() 62 | lay.addWidget(self._topWidget) 63 | 64 | searchWidget = QWidget() 65 | searchWidget.setLayout(lay) 66 | lay.setContentsMargins(0, 0, 0, 0) 67 | 68 | lay = QGridLayout() 69 | lay.addWidget(searchWidget) 70 | lay.setContentsMargins(0, 0, 0, 0) 71 | 72 | self.__setStyle() 73 | 74 | self.setLayout(lay) 75 | 76 | # ex) searchBar.setLabel(True, 'Search Text') 77 | def setLabel(self, visibility: bool = True, text=None): 78 | if text: 79 | self.__label.setText(text) 80 | self.__label.setVisible(visibility) 81 | 82 | def __setStyle(self): 83 | self.__searchIcon.load(os.path.join(os.path.dirname(__file__), '../ico/search.svg')) 84 | # set style sheet 85 | with open(os.path.join(os.path.dirname(__file__), '../style/lineedit.css'), 'r') as f: 86 | self.__searchLineEdit.setStyleSheet(f.read()) 87 | with open(os.path.join(os.path.dirname(__file__), '../style/search_bar.css'), 'r') as f: 88 | self.__searchBar.setStyleSheet(f.read()) 89 | with open(os.path.join(os.path.dirname(__file__), '../style/widget.css'), 'r') as f: 90 | self.setStyleSheet(f.read()) 91 | 92 | def __searched(self, text): 93 | self.searched.emit(text) 94 | 95 | def setSearchIcon(self, icon_filename: str): 96 | self.__searchIcon.load(icon_filename) 97 | 98 | def setPlaceHolder(self, text: str): 99 | self.__searchLineEdit.setPlaceholderText(text) 100 | 101 | def getSearchBar(self): 102 | return self.__searchLineEdit 103 | 104 | def getSearchLabel(self): 105 | return self.__searchIcon 106 | 107 | def showEvent(self, e): 108 | self.__searchLineEdit.setFocus() 109 | 110 | 111 | # for search feature 112 | class FilterProxyModel(QSortFilterProxyModel): 113 | def __init__(self): 114 | super().__init__() 115 | self.__searchedText = '' 116 | 117 | @property 118 | def searchedText(self): 119 | return self.__searchedText 120 | 121 | @searchedText.setter 122 | def searchedText(self, value): 123 | self.__searchedText = value 124 | self.invalidateFilter() 125 | 126 | 127 | # for align text in every cell to center 128 | class AlignDelegate(QStyledItemDelegate): 129 | def initStyleOption(self, option, index): 130 | super().initStyleOption(option, index) 131 | option.displayAlignment = Qt.AlignCenter 132 | 133 | def createEditor(self, parent, option, index): 134 | editor = super().createEditor(parent, option, index) 135 | c = index.column() 136 | # 100000 137 | if c > 3: 138 | validator = QIntValidator() 139 | editor.setValidator(validator) 140 | return editor 141 | 142 | class SqlTableModel(QSqlTableModel): 143 | added = Signal(int, str) 144 | updated = Signal(int, str) 145 | deleted = Signal(list) 146 | addedCol = Signal() 147 | deletedCol = Signal() 148 | 149 | def flags(self, index: Union[QModelIndex, QPersistentModelIndex]) -> Qt.ItemFlags: 150 | if index.column() == 0: 151 | return Qt.ItemIsEnabled | Qt.ItemIsSelectable 152 | return super().flags(index) 153 | 154 | class DatabaseWidget(QWidget): 155 | 156 | def __init__(self): 157 | super().__init__() 158 | self.__initUi() 159 | 160 | def __initUi(self): 161 | # table name 162 | self.__tableName = "contacts" 163 | 164 | # label 165 | lbl = QLabel(self.__tableName.capitalize()) 166 | 167 | columnNames = ['ID', 'Name', 'Job', 'Email', 'Score 1', 'Score 2', 'Score 3'] 168 | 169 | # database table 170 | # set up the model 171 | self.__model = SqlTableModel(self) 172 | self.__model.setTable(self.__tableName) 173 | self.__model.setEditStrategy(QSqlTableModel.OnFieldChange) 174 | self.__model.beforeUpdate.connect(self.__updated) 175 | for i in range(len(columnNames)): 176 | self.__model.setHeaderData(i, Qt.Horizontal, columnNames[i]) 177 | self.__model.select() 178 | 179 | # init the proxy model 180 | self.__proxyModel = FilterProxyModel() 181 | 182 | # set the table model as source model to make it enable to feature sort and filter function 183 | self.__proxyModel.setSourceModel(self.__model) 184 | 185 | # set up the view 186 | self.__tableView = QTableView() 187 | self.__tableView.setModel(self.__proxyModel) 188 | 189 | # align to center 190 | delegate = AlignDelegate() 191 | for i in range(self.__model.columnCount()): 192 | self.__tableView.setItemDelegateForColumn(i, delegate) 193 | 194 | # set selection/resize policy 195 | self.__tableView.setSelectionBehavior(QAbstractItemView.SelectRows) 196 | self.__tableView.resizeColumnsToContents() 197 | self.__tableView.setSelectionMode(QAbstractItemView.ExtendedSelection) 198 | 199 | # sort (ascending order by default) 200 | self.__tableView.setSortingEnabled(True) 201 | self.__tableView.sortByColumn(0, Qt.AscendingOrder) 202 | 203 | # set current index as first record 204 | self.__tableView.setCurrentIndex(self.__tableView.model().index(0, 0)) 205 | 206 | # add/delete record buttons 207 | addBtn = QPushButton('Add Record') 208 | addBtn.clicked.connect(self.__add) 209 | self.__delBtn = QPushButton('Delete Record') 210 | self.__delBtn.clicked.connect(self.__delete) 211 | 212 | # column add/delete buttons 213 | addColBtn = QPushButton('Add Column') 214 | addColBtn.clicked.connect(self.__addCol) 215 | self.__delColBtn = QPushButton('Delete Column') 216 | self.__delColBtn.clicked.connect(self.__deleteCol) 217 | 218 | self.__importBtn = QPushButton('Import As Excel') 219 | self.__importBtn.clicked.connect(self.__import) 220 | 221 | self.__exportBtn = QPushButton('Export As Excel') 222 | self.__exportBtn.clicked.connect(self.__export) 223 | 224 | # instant search bar 225 | self.__searchBar = InstantSearchBar() 226 | self.__searchBar.setPlaceHolder('Search...') 227 | self.__searchBar.searched.connect(self.__showResult) 228 | 229 | # combo box to make it enable to search by each column 230 | self.__comboBox = QComboBox() 231 | items = ['All'] + columnNames 232 | for i in range(len(items)): 233 | self.__comboBox.addItem(items[i]) 234 | self.__comboBox.currentIndexChanged.connect(self.__currentIndexChanged) 235 | 236 | # show table info 237 | self.__tableInfoWidget = QTableWidget() 238 | self.__setTableInfo(schema_name='contacts.sqlite', table_name=self.__tableName) 239 | 240 | # set layout 241 | lay = QHBoxLayout() 242 | lay.addWidget(lbl) 243 | lay.addSpacerItem(QSpacerItem(10, 10, QSizePolicy.MinimumExpanding)) 244 | lay.addWidget(self.__searchBar) 245 | lay.addWidget(self.__comboBox) 246 | lay.addWidget(addBtn) 247 | lay.addWidget(self.__delBtn) 248 | lay.addWidget(addColBtn) 249 | lay.addWidget(self.__delColBtn) 250 | lay.addWidget(self.__importBtn) 251 | lay.addWidget(self.__exportBtn) 252 | lay.setContentsMargins(0, 0, 0, 0) 253 | btnWidget = QWidget() 254 | btnWidget.setLayout(lay) 255 | 256 | lay = QVBoxLayout() 257 | lay.addWidget(btnWidget) 258 | lay.addWidget(self.__tableView) 259 | lay.addWidget(QLabel('Table Info')) 260 | lay.addWidget(self.__tableInfoWidget) 261 | 262 | self.setLayout(lay) 263 | 264 | # show default result (which means "show all") 265 | self.__showResult('') 266 | 267 | # init delete button enabled 268 | self.__delBtnToggle() 269 | 270 | def __setTableInfo(self, schema_name: str, table_name: str): 271 | conn = sqlite3.connect('contacts.sqlite') 272 | cur = conn.cursor() 273 | result = cur.execute(f'PRAGMA table_info([{self.__tableName}])') 274 | result_info = result.fetchall() 275 | df = pd.DataFrame(result_info, columns=['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk']) 276 | columnNames = df.keys().values 277 | values = df.values 278 | self.__tableInfoWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) 279 | self.__tableInfoWidget.setColumnCount(len(columnNames)) 280 | self.__tableInfoWidget.setRowCount(len(values)) 281 | 282 | for i in range(len(columnNames)): 283 | self.__tableInfoWidget.setHorizontalHeaderItem(i, QTableWidgetItem(columnNames[i])) 284 | for i in range(len(values)): 285 | for j in range(len(values[i])): 286 | self.__tableInfoWidget.setItem(i, j, QTableWidgetItem(str(values[i][j]))) 287 | 288 | delegate = AlignDelegate() 289 | for i in range(self.__tableInfoWidget.columnCount()): 290 | self.__tableInfoWidget.setItemDelegateForColumn(i, delegate) 291 | 292 | def __delBtnToggle(self): 293 | self.__delBtn.setEnabled(len(self.__tableView.selectedIndexes()) > 0) 294 | 295 | def __add(self): 296 | # add new record 297 | r = self.__model.record() 298 | r.setValue("name", '') 299 | r.setValue("job", '') 300 | r.setValue("email", '') 301 | self.__model.insertRecord(-1, r) 302 | self.__model.select() 303 | 304 | # set new record as current index 305 | newRecordIdx = self.__tableView.model().index(self.__tableView.model().rowCount() - 1, 0) 306 | self.__tableView.setCurrentIndex(newRecordIdx) 307 | 308 | # send add signal 309 | id = newRecordIdx.data() 310 | self.__model.added.emit(id, r.value('name')) 311 | 312 | # make the record editable right after being added 313 | self.__tableView.edit(self.__tableView.currentIndex().siblingAtColumn(1)) 314 | self.__delBtnToggle() 315 | 316 | def __updated(self, i, r): 317 | # send updated signal 318 | self.__model.updated.emit(r.value('id'), r.value('name')) 319 | 320 | def __delete(self): 321 | # delete select rows(records) 322 | rows = [idx.row() for idx in self.__tableView.selectedIndexes()] 323 | names = [] 324 | for r_idx in rows: 325 | name = self.__model.data(self.__model.index(r_idx, 1)) 326 | if name: 327 | names.append(name) 328 | self.__model.removeRow(r_idx) 329 | self.__model.select() 330 | 331 | # set previous row of first removed one as current index 332 | self.__tableView.setCurrentIndex(self.__tableView.model().index(max(0, rows[0] - 1), 0)) 333 | 334 | # send deleted signal 335 | self.__model.deleted.emit(names) 336 | self.__delBtnToggle() 337 | 338 | def __import(self): 339 | filename = QFileDialog.getOpenFileName(self, 'Select the file', '', 'Excel File (*.xlsx)') 340 | filename = filename[0] 341 | if filename: 342 | try: 343 | con = sqlite3.connect('contacts.sqlite') 344 | wb = pd.read_excel(filename, sheet_name=None) 345 | for sheet in wb: 346 | wb[sheet].to_sql(sheet, con, index=False) 347 | con.commit() 348 | con.close() 349 | self.__model.setTable('Sheet1') 350 | self.__model.select() 351 | except Exception as e: 352 | print(e) 353 | 354 | def __addCol(self): 355 | dialog = AddColDialog() 356 | reply = dialog.exec() 357 | if reply == QDialog.Accepted: 358 | q = QSqlQuery() 359 | q.prepare(f'ALTER TABLE {self.__tableName} ADD COLUMN "{dialog.getColumnName()}" INTEGER') 360 | q.exec() 361 | self.__model.setTable(self.__tableName) 362 | self.__model.select() 363 | self.__tableView.resizeColumnsToContents() 364 | self.__model.addedCol.emit() 365 | 366 | # align to center 367 | delegate = AlignDelegate() 368 | for i in range(self.__model.columnCount()): 369 | self.__tableView.setItemDelegateForColumn(i, delegate) 370 | 371 | def __deleteCol(self): 372 | dialog = DelColDialog(self.__tableName) 373 | reply = dialog.exec() 374 | if reply == QDialog.Accepted: 375 | columnNamesToRemove = dialog.getColumnNames() 376 | conn = sqlite3.connect('contacts.sqlite') 377 | cur = conn.cursor() 378 | 379 | # choose the column names except for ones which are supposed to be removed 380 | mysel = cur.execute(f"select * from {self.__tableName}") 381 | columnNames = list(map(lambda x: x[0], mysel.description)) 382 | columnNames = list(filter(lambda c: c not in columnNamesToRemove, columnNames)) 383 | 384 | # q = QSqlQuery() 385 | # # TODO check if the name which is about to be set exists 386 | # q.prepare(f'ALTER TABLE {self.__tableName} RENAME TO {self.__tableName}2') 387 | # q.exec() 388 | # # TODO refactoring this part 389 | # q.prepare(f'CREATE TABLE {self.__tableName}') 390 | # q.exec() 391 | 392 | # self.__model.setTable(self.__tableName) 393 | # self.__model.select() 394 | # self.__tableView.resizeColumnsToContents() 395 | # self.__model.deletedCol.emit() 396 | 397 | def __export(self): 398 | filename = QFileDialog.getSaveFileName(self, 'Export', '.', 'Excel File (*.xlsx)') 399 | filename = filename[0] 400 | if filename: 401 | workbook = xlsxwriter.Workbook(filename) 402 | worksheet = workbook.add_worksheet() 403 | conn = sqlite3.connect('contacts.sqlite') 404 | cur = conn.cursor() 405 | 406 | mysel = cur.execute(f"select * from {self.__tableName}") 407 | columnNames = list(map(lambda x: x[0], mysel.description)) 408 | 409 | for c_idx in range(len(columnNames)): 410 | worksheet.write(0, c_idx, columnNames[c_idx]) 411 | 412 | for i, row in enumerate(mysel): 413 | for j, value in enumerate(row): 414 | worksheet.write(i+1, j, row[j]) 415 | workbook.close() 416 | 417 | path = filename.replace('/', '\\') 418 | subprocess.Popen(r'explorer /select,"' + path + '"') 419 | 420 | def __showResult(self, text): 421 | # index -1 will be read from all columns 422 | # otherwise it will be read the current column number indicated by combobox 423 | self.__proxyModel.setFilterKeyColumn(self.__comboBox.currentIndex() - 1) 424 | # regular expression can be used 425 | self.__proxyModel.setFilterRegularExpression(text) 426 | 427 | def __currentIndexChanged(self, idx): 428 | self.__showResult(self.__searchBar.getSearchBar().text()) 429 | 430 | def getModel(self): 431 | return self.__model 432 | 433 | def getView(self): 434 | return self.__tableView 435 | 436 | 437 | def createConnection(): 438 | con = QSqlDatabase.addDatabase("QSQLITE") 439 | con.setDatabaseName("contacts.sqlite") 440 | if not con.open(): 441 | QMessageBox.critical( 442 | None, 443 | "QTableView Example - Error!", 444 | "Database Error: %s" % con.lastError().databaseText(), 445 | ) 446 | return False 447 | return True 448 | 449 | 450 | def initTable(): 451 | table = 'contacts' 452 | 453 | dropTableQuery = QSqlQuery() 454 | dropTableQuery.prepare( 455 | f'DROP TABLE {table}' 456 | ) 457 | dropTableQuery.exec() 458 | 459 | createTableQuery = QSqlQuery() 460 | createTableQuery.prepare( 461 | f""" 462 | CREATE TABLE {table} ( 463 | ID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, 464 | Name VARCHAR(40) UNIQUE NOT NULL, 465 | Job VARCHAR(50), 466 | Email VARCHAR(40) NOT NULL, 467 | "Score 1" INTEGER, 468 | "Score 2" INTEGER, 469 | "Score 3" INTEGER 470 | ) 471 | """ 472 | ) 473 | createTableQuery.exec() 474 | 475 | 476 | def addSample(): 477 | table = 'contacts' 478 | 479 | insertDataQuery = QSqlQuery() 480 | insertDataQuery.prepare( 481 | f""" 482 | INSERT INTO {table} ( 483 | Name, 484 | Job, 485 | Email, 486 | "Score 1", 487 | "Score 2", 488 | "Score 3" 489 | ) 490 | VALUES (?, ?, ?, ?, ?, ?) 491 | """ 492 | ) 493 | 494 | # Sample data 495 | data = [ 496 | ("Joe", "Senior Web Developer", "joe@example.com", "251", "112", "315"), 497 | ("Lara", "Project Manager", "lara@example.com", "325", "231", "427"), 498 | ("David", "Data Analyst", "david@example.com", "341", "733", "502"), 499 | ("Jane", "Senior Python Developer", "jane@example.com", "310", "243", "343"), 500 | ] 501 | 502 | # Use .addBindValue() to insert data 503 | for name, job, email, score1, score2, score3 in data: 504 | insertDataQuery.addBindValue(name) 505 | insertDataQuery.addBindValue(job) 506 | insertDataQuery.addBindValue(email) 507 | insertDataQuery.addBindValue(score1) 508 | insertDataQuery.addBindValue(score2) 509 | insertDataQuery.addBindValue(score3) 510 | insertDataQuery.exec() -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/db/delColDialog.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | from PySide6.QtWidgets import QDialog, QGroupBox, QPushButton, QHBoxLayout, QWidget, QVBoxLayout, QCheckBox 4 | 5 | 6 | class DelColDialog(QDialog): 7 | def __init__(self, table_name): 8 | super().__init__() 9 | self.__initVal() 10 | self.__initUi(table_name) 11 | 12 | def __initVal(self): 13 | self.__chkBoxes = [] 14 | 15 | def __initUi(self, table_name): 16 | lay = QVBoxLayout() 17 | 18 | conn = sqlite3.connect('contacts.sqlite') 19 | cur = conn.cursor() 20 | 21 | mysel = cur.execute(f"select * from {table_name}") 22 | columnNames = list(map(lambda x: x[0], mysel.description)) 23 | 24 | columnNames.remove('ID') 25 | columnNames.remove('Name') 26 | columnNames.remove('Job') 27 | columnNames.remove('Email') 28 | 29 | for columnName in columnNames: 30 | chkBox = QCheckBox(columnName) 31 | self.__chkBoxes.append(chkBox) 32 | lay.addWidget(chkBox) 33 | 34 | groupBox = QGroupBox() 35 | groupBox.setLayout(lay) 36 | 37 | self.__okBtn = QPushButton('OK') 38 | self.__okBtn.clicked.connect(self.accept) 39 | 40 | closeBtn = QPushButton('Close') 41 | closeBtn.clicked.connect(self.close) 42 | 43 | lay = QHBoxLayout() 44 | lay.addWidget(self.__okBtn) 45 | lay.addWidget(closeBtn) 46 | lay.setContentsMargins(0, 0, 0, 0) 47 | 48 | bottomWidget = QWidget() 49 | bottomWidget.setLayout(lay) 50 | 51 | lay = QVBoxLayout() 52 | lay.addWidget(groupBox) 53 | lay.addWidget(bottomWidget) 54 | 55 | self.setLayout(lay) 56 | 57 | self.setFixedSize(self.sizeHint().width(), self.sizeHint().height()) 58 | 59 | def getColumnNames(self): 60 | return [checkbox.text() for checkbox in self.__chkBoxes if checkbox.isChecked()] 61 | -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/file_sample/a.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjg30737/pyside-db-chart-mapping-example/176ee043aa7e031fa91531b591cb20fbd356bb7f/pyside_db_chart_mapping_example/file_sample/a.pdf -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/file_sample/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjg30737/pyside-db-chart-mapping-example/176ee043aa7e031fa91531b591cb20fbd356bb7f/pyside_db_chart_mapping_example/file_sample/a.png -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/file_sample/a.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjg30737/pyside-db-chart-mapping-example/176ee043aa7e031fa91531b591cb20fbd356bb7f/pyside_db_chart_mapping_example/file_sample/a.xlsx -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/ico/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjg30737/pyside-db-chart-mapping-example/176ee043aa7e031fa91531b591cb20fbd356bb7f/pyside_db_chart_mapping_example/ico/__init__.py -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/ico/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6.QtWidgets import QMainWindow, QSplitter, QListWidgetItem, QListWidget, QCheckBox 4 | 5 | from pyside_db_chart_mapping_example.chart.chart import ChartWidget 6 | from pyside_db_chart_mapping_example.db.db import * 7 | 8 | 9 | class CheckBoxListWidget(QListWidget): 10 | checkedSignal = Signal(int, Qt.CheckState) 11 | 12 | def __init__(self): 13 | super().__init__() 14 | self.itemChanged.connect(self.__sendCheckedSignal) 15 | 16 | def __sendCheckedSignal(self, item): 17 | r_idx = self.row(item) 18 | state = item.checkState() 19 | self.checkedSignal.emit(r_idx, state) 20 | 21 | def addItems(self, items) -> None: 22 | for item in items: 23 | self.addItem(item) 24 | 25 | def addItem(self, item) -> None: 26 | if isinstance(item, str): 27 | item = QListWidgetItem(item) 28 | item.setFlags(item.flags() | Qt.ItemIsUserCheckable) 29 | super().addItem(item) 30 | 31 | def toggleState(self, state): 32 | state = Qt.Checked if state == 2 else Qt.Unchecked 33 | for i in range(self.count()): 34 | item = self.item(i) 35 | item.setCheckState(state) 36 | 37 | def getCheckedRows(self): 38 | return self.__getFlagRows(Qt.Checked) 39 | 40 | def getUncheckedRows(self): 41 | return self.__getFlagRows(Qt.Unchecked) 42 | 43 | def __getFlagRows(self, flag: Qt.CheckState): 44 | flag_lst = [] 45 | for i in range(self.count()): 46 | item = self.item(i) 47 | if item.checkState() == flag: 48 | flag_lst.append(i) 49 | 50 | return flag_lst 51 | 52 | def removeCheckedRows(self): 53 | self.__removeFlagRows(Qt.Checked) 54 | 55 | def removeUncheckedRows(self): 56 | self.__removeFlagRows(Qt.Unchecked) 57 | 58 | def __removeFlagRows(self, flag): 59 | flag_lst = self.__getFlagRows(flag) 60 | flag_lst = reversed(flag_lst) 61 | for i in flag_lst: 62 | self.takeItem(i) 63 | 64 | 65 | class CheckWidget(QWidget): 66 | itemChecked = Signal(int, Qt.CheckState) 67 | 68 | def __init__(self, label): 69 | super().__init__() 70 | self.__initUi(label) 71 | 72 | def __initUi(self, label): 73 | self.__checkBoxListWidget = CheckBoxListWidget() 74 | self.__checkBoxListWidget.checkedSignal.connect(self.itemChecked) 75 | 76 | self.__allCheckBox = QCheckBox('Check all') 77 | self.__allCheckBox.stateChanged.connect(self.__checkBoxListWidget.toggleState) 78 | self.__allCheckBox.setChecked(True) 79 | 80 | lay = QHBoxLayout() 81 | lay.addWidget(QLabel(label)) 82 | lay.addSpacerItem(QSpacerItem(10, 10, QSizePolicy.MinimumExpanding)) 83 | lay.addWidget(self.__allCheckBox) 84 | lay.setContentsMargins(0, 0, 0, 0) 85 | 86 | leftTopMenuWidget = QWidget() 87 | leftTopMenuWidget.setLayout(lay) 88 | 89 | lay = QVBoxLayout() 90 | lay.addWidget(leftTopMenuWidget) 91 | lay.addWidget(self.__checkBoxListWidget) 92 | 93 | self.setLayout(lay) 94 | 95 | def addItems(self, items: list): 96 | for itemText in items: 97 | item = QListWidgetItem(itemText) 98 | item.setCheckState(self.__allCheckBox.checkState()) 99 | self.__checkBoxListWidget.addItem(item) 100 | 101 | def getItem(self, idx): 102 | return self.__checkBoxListWidget.item(idx) 103 | 104 | 105 | class BarsetItemCheckWidget(CheckWidget): 106 | def __init__(self): 107 | super().__init__('Barset') 108 | 109 | 110 | class CategoryCheckWidget(CheckWidget): 111 | def __init__(self): 112 | super().__init__('Categories') 113 | 114 | 115 | class Window(QMainWindow): 116 | def __init__(self): 117 | super().__init__() 118 | self.__initUi() 119 | 120 | def __initUi(self): 121 | if not createConnection(): 122 | sys.exit(1) 123 | initTable() 124 | addSample() 125 | 126 | self.__barsetCheckListWidget = BarsetItemCheckWidget() 127 | self.__barsetCheckListWidget.itemChecked.connect(self.__refreshSeries) 128 | 129 | self.__categoryCheckListWidget = CategoryCheckWidget() 130 | self.__categoryCheckListWidget.itemChecked.connect(self.__refreshCategory) 131 | 132 | leftWidget = QSplitter() 133 | leftWidget.setOrientation(Qt.Vertical) 134 | leftWidget.addWidget(self.__barsetCheckListWidget) 135 | leftWidget.addWidget(self.__categoryCheckListWidget) 136 | leftWidget.setChildrenCollapsible(False) 137 | leftWidget.setHandleWidth(1) 138 | leftWidget.setStyleSheet( 139 | "QSplitterHandle {background-color: lightgray;}") 140 | 141 | dbWidget = DatabaseWidget() 142 | chartWidget = ChartWidget() 143 | model = dbWidget.getModel() 144 | chartWidget.mapDbModel(model) 145 | 146 | self.__barsetCheckListWidget.addItems(chartWidget.getBarsetsTextList()) 147 | self.__categoryCheckListWidget.addItems(chartWidget.getCategories()) 148 | 149 | mainWidget = QSplitter() 150 | mainWidget.addWidget(leftWidget) 151 | mainWidget.addWidget(dbWidget) 152 | mainWidget.addWidget(chartWidget) 153 | mainWidget.setChildrenCollapsible(False) 154 | mainWidget.setSizes([200, 500, 600]) 155 | mainWidget.setHandleWidth(1) 156 | mainWidget.setStyleSheet( 157 | "QSplitterHandle {background-color: lightgray;}") 158 | 159 | self.setCentralWidget(mainWidget) 160 | 161 | # fixme 162 | # Internal C++ object (PySide6.QtCharts.QBarSet) already deleted. 163 | # Update the mapper to solve this problem 164 | def __refreshSeries(self, idx, checked): 165 | # itemText = self.__barsetCheckListWidget.getItem(idx).text() 166 | # for i in range(self.__model.rowCount()): 167 | # if barset.label() == itemText: 168 | # if checked == Qt.Checked: 169 | # pass 170 | # # self.__series.insert(idx, barset) 171 | # else: 172 | # self.__series.remove(barset) 173 | # break 174 | pass 175 | 176 | def __refreshCategory(self, idx, checked): 177 | # itemText = self.__axisCheckBoxListWidget.getItem(idx).text() 178 | # self.__axisX.categories() 179 | # barsets = [ for barset in self.__chart.axisX(self.__series)] 180 | # for barset in barsets: 181 | # if barset.label() == itemText: 182 | # if checked == Qt.Checked: 183 | # self.__series.insert(idx, barset) 184 | # else: 185 | # self.__series.remove(barset) 186 | # break 187 | pass 188 | 189 | 190 | if __name__ == "__main__": 191 | app = QApplication(sys.argv) 192 | ex = Window() 193 | ex.show() 194 | app.exec() 195 | -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjg30737/pyside-db-chart-mapping-example/176ee043aa7e031fa91531b591cb20fbd356bb7f/pyside_db_chart_mapping_example/style/__init__.py -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/black_overlay.css: -------------------------------------------------------------------------------- 1 | QWidget 2 | { 3 | background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(0, 0, 0, 0), 4 | stop:1 rgba(0, 0, 0, 255)); 5 | width:100%; 6 | border-radius: 5px; 7 | } -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/black_ring_of_color_selector.css: -------------------------------------------------------------------------------- 1 | QLabel 2 | { 3 | background-color: none; 4 | border: 1px solid black; 5 | border-radius: 5px; 6 | } -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/color_selector.css: -------------------------------------------------------------------------------- 1 | QWidget 2 | { 3 | background-color: none; 4 | border: 1px solid white; 5 | border-radius: 5px; 6 | } -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/hue_bg.css: -------------------------------------------------------------------------------- 1 | QWidget 2 | { 3 | background-color: qlineargradient(spread:pad, 4 | x1:0, y1:1, x2:0, y2:0, 5 | stop:0 rgba(255, 0, 0, 255), 6 | stop:0.166 rgba(255, 0, 255, 255), 7 | stop:0.333 rgba(0, 0, 255, 255), 8 | stop:0.5 rgba(0, 255, 255, 255), 9 | stop:0.666 rgba(0, 255, 0, 255), 10 | stop:0.833 rgba(255, 255, 0, 255), 11 | stop:1 rgba(255, 0, 0, 255)); 12 | border-radius: 5px; 13 | } -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/hue_frame.css: -------------------------------------------------------------------------------- 1 | QLabel 2 | { 3 | border-radius: 5px; 4 | } -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/hue_selector.css: -------------------------------------------------------------------------------- 1 | QLabel 2 | { 3 | background-color: white; border: 2px solid #222; 4 | } -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/lineedit.css: -------------------------------------------------------------------------------- 1 | QLineEdit 2 | { 3 | background: transparent; 4 | color: #333333; 5 | border: none; 6 | } -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/search_bar.css: -------------------------------------------------------------------------------- 1 | QWidget#searchBar 2 | { 3 | border: 1px solid gray; 4 | } -------------------------------------------------------------------------------- /pyside_db_chart_mapping_example/style/widget.css: -------------------------------------------------------------------------------- 1 | QWidget { padding: 5px; } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | xlsxwriter 2 | pandas 3 | openpyxl -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='pyside-db-chart-mapping-example', 5 | version='0.0.38', 6 | author='Jung Gyu Yoon', 7 | author_email='yjg30737@gmail.com', 8 | license='MIT', 9 | packages=find_packages(), 10 | package_data={'pyside_db_chart_mapping_example.ico': ['search.svg'], 11 | 'pyside_db_chart_mapping_example.style': ['lineedit.css', 'search_bar.css', 'widget.css', 12 | 'black_overlay.css', 13 | 'black_ring_of_color_selector.css', 14 | 'color_selector.css', 15 | 'hue_bg.css', 16 | 'hue_frame.css', 17 | 'hue_selector.css']}, 18 | description='PySide6 example of mapping database table(QSqlTableModel based table view) and chart with QVBarModelMapper', 19 | url='https://github.com/yjg30737/pyside-db-chart-mapping-example.git', 20 | install_requires=[ 21 | 'PySide6', 22 | 'xlsxwriter', 23 | 'pandas', 24 | 'openpyxl' 25 | ] 26 | ) --------------------------------------------------------------------------------