├── .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 |
4 |
5 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 | 
57 |
58 | You don't have to care about left check box list. I'm still working on it.
59 |
60 | 
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 |
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 | )
--------------------------------------------------------------------------------