├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── examples ├── mainwindow.png ├── mainwindow.py └── mainwindow.ui ├── qtmodern ├── __init__.py ├── _utils.py ├── resources │ ├── frameless.qss │ └── style.qss ├── styles.py └── windows.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # editors 2 | **/*.swp 3 | 4 | # python files 5 | **/*.pyc 6 | **/__pycache__ 7 | build/ 8 | dist/ 9 | *.egg* 10 | 11 | # os 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | script: 4 | - python setup.py sdist 5 | 6 | deploy: 7 | provider: pypi 8 | user: gmarull 9 | password: 10 | secure: 45NtJr56shHRiwcOJoUI7pKDf1HVzSEqItmpfsYf7lplZLyJ+5NMox5Sb6SA0KWUoShI1TNWyRl8VfLiwayEN4AxmwT6e8a2XYX1DPwMAjsnVb8yD9PY4wTeIpizC5sO1KB9LadmM8fX/j9Ovy07lAs/yfcWYVMfMyDZL5Z3jqqsJbASMXfyUsD+mzPjtPagAX9jq0WFs66AfDenob0ZIqcL9SOVNHytFvpwzzbvUh+WBRucjgHjqpoPWFWjKKMDQ58q4O5A9cTLd3kzZuBBzbj4ZDBxZouMfWO44B6aFw6OnBvuTpqpZ7ro6jvRZwYdWPRs8lLy3lkuW+e71VIF1SbwJw8GDYF99A5U3FqYN4JmN8bV/3DVbVOYvIteThanCx2GNdJMaaxWfAah9kQV8A7mF177niF0t/odgvHImHxXxhJdATMPzj3ZUJle/V+sRoRciK55Vman29xPYrzZnSIyWGSVEInSuZ05mao9OtAqk+nyelVORBd6g8fhqj1+GxPD7w3dlSMPKS/uTfaBlnEjQD8EpC/GTJ7BrGPZsixqgYGtjyiTZFpMRxbFUsawxxfKlrBFz96kOs0ZpaWBKGJym5lBltsBOoM8UjhFbcFuHCSjYPPcOlMI7HnHESNH5H1iKZKbRzlZ+wMmagdwiXQBqbh+LNEgEGoN7/Ou1x0= 11 | on: 12 | branch: master 13 | tags: true 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gerard Marull-Paretas 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | qtmodern 3 | ======== 4 | 5 | .. image:: https://travis-ci.org/gmarull/qtmodern.svg?branch=master 6 | :target: https://travis-ci.org/gmarull/qtmodern 7 | :alt: Travis build 8 | 9 | .. image:: https://img.shields.io/pypi/v/qtmodern.svg 10 | :target: https://pypi.python.org/pypi/qtmodern 11 | :alt: PyPI Version 12 | 13 | ``qtmodern`` is a Python package aimed to make PyQt/PySide applications look 14 | better and consistent on multiple platforms. It provides a custom frameless 15 | window and a dark theme. In order to be compatible with multiple Python Qt 16 | wrappers `QtPy `_ is used. The initial idea 17 | comes from `this project `_. 18 | 19 | .. image:: examples/mainwindow.png 20 | :width: 450px 21 | :align: center 22 | :alt: Example 23 | 24 | Installation 25 | ------------ 26 | 27 | The recommended way to install is by using ``pip``, i.e:: 28 | 29 | pip install qtmodern 30 | 31 | Usage 32 | ----- 33 | 34 | In order to use ``qtmodern``, simply apply the style you want to your 35 | application and then, create a ``ModernWindow`` enclosing the window you want to 36 | *modernize*:: 37 | 38 | import qtmodern.styles 39 | import qtmodern.windows 40 | 41 | ... 42 | 43 | app = QApplication() 44 | win = YourWindow() 45 | 46 | qtmodern.styles.dark(app) 47 | mw = qtmodern.windows.ModernWindow(win) 48 | mw.show() 49 | 50 | ... 51 | 52 | -------------------------------------------------------------------------------- /examples/mainwindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmarull/qtmodern/734a3f3be92e2cba8de42e602aa5a8b8b7a4ae34/examples/mainwindow.png -------------------------------------------------------------------------------- /examples/mainwindow.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from time import sleep 3 | from os.path import join, dirname, abspath, basename, isdir 4 | from os import listdir 5 | 6 | from qtpy import uic 7 | from qtpy.QtCore import Slot, QThread, Signal 8 | from qtpy.QtWidgets import QApplication, QMainWindow, QMessageBox, QTreeWidgetItem 9 | 10 | import qtmodern.styles 11 | import qtmodern.windows 12 | 13 | 14 | _UI = join(dirname(abspath(__file__)), 'mainwindow.ui') 15 | 16 | 17 | class ProgressThread(QThread): 18 | update = Signal(int) 19 | 20 | def run(self): 21 | progress = 20 22 | while True: 23 | progress += 1 24 | if progress == 100: 25 | progress = 0 26 | 27 | self.update.emit(progress) 28 | sleep(0.5) 29 | 30 | 31 | class MainWindow(QMainWindow): 32 | def __init__(self): 33 | QMainWindow.__init__(self) 34 | 35 | uic.loadUi(_UI, self) # Load the ui into self 36 | 37 | self.tooltiplabel.setToolTip("This is a tool tip that shows a tip about the tool") 38 | 39 | self.actionLight.triggered.connect(self.lightTheme) 40 | self.actionDark.triggered.connect(self.darkTheme) 41 | 42 | self.thread = ProgressThread() 43 | self.thread.update.connect(self.update_progress) 44 | self.thread.start() 45 | 46 | self.load_project_structure(dirname(dirname(abspath(__file__))), self.treeWidget) 47 | 48 | for i in range(100): 49 | self.comboBox_2.addItem("item {}".format(i)) 50 | 51 | def load_project_structure(self, startpath, tree): 52 | for element in listdir(startpath): 53 | path_info = startpath + "/" + element 54 | parent_itm = QTreeWidgetItem(tree, [basename(element)]) 55 | if isdir(path_info): 56 | self.load_project_structure(path_info, parent_itm) 57 | 58 | def update_progress(self, progress): 59 | self.progressBar.setValue(progress) 60 | 61 | def lightTheme(self): 62 | qtmodern.styles.light(QApplication.instance()) 63 | 64 | def darkTheme(self): 65 | qtmodern.styles.dark(QApplication.instance()) 66 | 67 | @Slot() 68 | def on_pushButton_clicked(self): 69 | self.close() 70 | 71 | @Slot() 72 | def closeEvent(self, event): 73 | reply = QMessageBox.question(self, 'Exit', 'Do you want to exit?') 74 | 75 | if reply == QMessageBox.Yes: 76 | event.accept() 77 | else: 78 | event.ignore() 79 | 80 | if __name__ == '__main__': 81 | app = QApplication(sys.argv) 82 | 83 | qtmodern.styles.dark(app) 84 | mw = qtmodern.windows.ModernWindow(MainWindow()) 85 | mw.show() 86 | 87 | sys.exit(app.exec_()) 88 | -------------------------------------------------------------------------------- /examples/mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 854 10 | 895 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | QGroupBox With Checkbox 22 | 23 | 24 | true 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | QScrollbar 34 | 35 | 36 | 37 | 38 | 39 | 40 | QSlider 41 | 42 | 43 | 44 | 45 | 46 | 47 | 60 48 | 49 | 50 | Qt::Horizontal 51 | 52 | 53 | 54 | 55 | 56 | 57 | test 58 | 59 | 60 | QLineEdit::Password 61 | 62 | 63 | 64 | 65 | 66 | 67 | 25 68 | 69 | 70 | Qt::Horizontal 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 16777215 82 | 50 83 | 84 | 85 | 86 | 30 87 | 88 | 89 | 90 | 91 | 92 | 93 | QDial 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | QLabel 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | QComboBox 114 | 115 | 116 | 117 | 118 | New Item 2 119 | 120 | 121 | 122 | 123 | New Item 3 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | Qt::Horizontal 135 | 136 | 137 | 138 | 40 139 | 20 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | Disable Widgets 148 | 149 | 150 | false 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | QProgressBar 162 | 163 | 164 | 165 | 166 | 167 | 168 | 24 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | QLCDNumber 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 0 188 | 20 189 | 190 | 191 | 192 | 12345.000000000000000 193 | 194 | 195 | 12345 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | QPushButton 211 | 212 | 213 | 214 | 215 | 216 | 217 | QPushButton - Checkable 218 | 219 | 220 | true 221 | 222 | 223 | 224 | 225 | 226 | 227 | QPushButton - Flat 228 | 229 | 230 | true 231 | 232 | 233 | 234 | 235 | 236 | 237 | QPushButton - Default 238 | 239 | 240 | true 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | QGroupBox 251 | 252 | 253 | 254 | 255 | 256 | QCheckBox - Unchecked 257 | 258 | 259 | true 260 | 261 | 262 | false 263 | 264 | 265 | true 266 | 267 | 268 | 269 | 270 | 271 | 272 | QRadioButton 273 | 274 | 275 | 276 | 277 | 278 | 279 | QCheckBox - Checked 280 | 281 | 282 | true 283 | 284 | 285 | true 286 | 287 | 288 | true 289 | 290 | 291 | 292 | 293 | 294 | 295 | -10 296 | 297 | 298 | QRadioButton 299 | 300 | 301 | true 302 | 303 | 304 | 305 | 306 | 307 | 308 | QFrame::Box 309 | 310 | 311 | QFrame::Raised 312 | 313 | 314 | 2 315 | 316 | 317 | Hover for toolip 318 | 319 | 320 | Qt::AlignCenter 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | true 337 | 338 | 339 | 340 | 1 341 | 342 | 343 | 344 | 345 | 2 346 | 347 | 348 | 349 | 350 | 3 351 | 352 | 353 | 354 | 355 | 4 356 | 357 | 358 | 359 | 360 | 5 361 | 362 | 363 | 364 | 365 | 6 366 | 367 | 368 | 369 | 370 | 7 371 | 372 | 373 | 374 | 375 | 8 376 | 377 | 378 | 379 | 380 | 9 381 | 382 | 383 | 384 | 385 | 10 386 | 387 | 388 | 389 | 390 | 1 391 | 392 | 393 | 394 | 395 | 2 396 | 397 | 398 | 399 | 400 | 3 401 | 402 | 403 | 404 | 405 | 4 406 | 407 | 408 | 409 | 410 | 5 411 | 412 | 413 | 414 | 415 | 6 416 | 417 | 418 | 419 | 420 | 7 421 | 422 | 423 | 424 | 425 | 8 426 | 427 | 428 | 429 | 430 | 9 431 | 432 | 433 | 434 | 435 | 10 436 | 437 | 438 | 439 | 440 | Cell 1/1 441 | 442 | 443 | 444 | 445 | Cell 2/1 446 | 447 | 448 | 449 | 450 | Cell 1/2 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | QTableView - Alternating Rows 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 1 476 | 477 | 478 | 479 | 480 | 2 481 | 482 | 483 | 484 | 485 | 3 486 | 487 | 488 | 489 | 490 | 4 491 | 492 | 493 | 494 | 495 | 5 496 | 497 | 498 | 499 | 500 | 6 501 | 502 | 503 | 504 | 505 | 7 506 | 507 | 508 | 509 | 510 | 8 511 | 512 | 513 | 514 | 515 | 9 516 | 517 | 518 | 519 | 520 | 10 521 | 522 | 523 | 524 | 525 | 1 526 | 527 | 528 | 529 | 530 | 2 531 | 532 | 533 | 534 | 535 | 3 536 | 537 | 538 | 539 | 540 | 4 541 | 542 | 543 | 544 | 545 | 5 546 | 547 | 548 | 549 | 550 | 6 551 | 552 | 553 | 554 | 555 | 7 556 | 557 | 558 | 559 | 560 | 8 561 | 562 | 563 | 564 | 565 | 9 566 | 567 | 568 | 569 | 570 | 10 571 | 572 | 573 | 574 | 575 | Cell 1/1 576 | 577 | 578 | 579 | 580 | Cell 2/1 581 | 582 | 583 | 584 | 585 | Cell 1/2 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | QTableView 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | QTreeWidget 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 0 613 | 614 | 615 | 616 | Tab - QTextEdit 617 | 618 | 619 | 620 | 621 | 622 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 623 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 624 | p, li { white-space: pre-wrap; } 625 | </style></head><body style=" font-family:'.SF NS Text'; font-size:13pt; font-weight:400; font-style:normal;"> 626 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br />test<br /></p></body></html> 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | Empty Page 635 | 636 | 637 | 638 | 639 | Empty Page 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 0 650 | 0 651 | 854 652 | 22 653 | 654 | 655 | 656 | 657 | Test 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | Test2 667 | 668 | 669 | 670 | Test2 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | Style 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | TopToolBarArea 691 | 692 | 693 | false 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | Test 703 | 704 | 705 | 706 | 707 | true 708 | 709 | 710 | true 711 | 712 | 713 | Test 714 | 715 | 716 | 717 | 718 | true 719 | 720 | 721 | Test 722 | 723 | 724 | 725 | 726 | Test3 727 | 728 | 729 | 730 | 731 | Test3 732 | 733 | 734 | 735 | 736 | Light 737 | 738 | 739 | 740 | 741 | Dark 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | checkBox_4 750 | toggled(bool) 751 | groupBox_2 752 | setDisabled(bool) 753 | 754 | 755 | 543 756 | 70 757 | 758 | 759 | 376 760 | 97 761 | 762 | 763 | 764 | 765 | checkBox_4 766 | toggled(bool) 767 | groupBox 768 | setDisabled(bool) 769 | 770 | 771 | 617 772 | 70 773 | 774 | 775 | 47 776 | 104 777 | 778 | 779 | 780 | 781 | checkBox_4 782 | toggled(bool) 783 | tabWidget 784 | setDisabled(bool) 785 | 786 | 787 | 623 788 | 74 789 | 790 | 791 | 234 792 | 264 793 | 794 | 795 | 796 | 797 | checkBox_4 798 | toggled(bool) 799 | groupBox_3 800 | setDisabled(bool) 801 | 802 | 803 | 503 804 | 80 805 | 806 | 807 | 503 808 | 262 809 | 810 | 811 | 812 | 813 | checkBox_4 814 | toggled(bool) 815 | progressBar 816 | setDisabled(bool) 817 | 818 | 819 | 631 820 | 73 821 | 822 | 823 | 607 824 | 517 825 | 826 | 827 | 828 | 829 | checkBox_4 830 | toggled(bool) 831 | mainToolBar 832 | setDisabled(bool) 833 | 834 | 835 | 648 836 | 71 837 | 838 | 839 | 105 840 | 33 841 | 842 | 843 | 844 | 845 | checkBox_4 846 | toggled(bool) 847 | menuBar 848 | setDisabled(bool) 849 | 850 | 851 | 634 852 | 73 853 | 854 | 855 | 64 856 | 7 857 | 858 | 859 | 860 | 861 | checkBox_4 862 | toggled(bool) 863 | statusBar 864 | setDisabled(bool) 865 | 866 | 867 | 650 868 | 70 869 | 870 | 871 | 533 872 | 549 873 | 874 | 875 | 876 | 877 | checkBox_4 878 | toggled(bool) 879 | comboBox 880 | setDisabled(bool) 881 | 882 | 883 | 626 884 | 70 885 | 886 | 887 | 297 888 | 65 889 | 890 | 891 | 892 | 893 | checkBox_4 894 | toggled(bool) 895 | label 896 | setDisabled(bool) 897 | 898 | 899 | 639 900 | 78 901 | 902 | 903 | 28 904 | 66 905 | 906 | 907 | 908 | 909 | 910 | -------------------------------------------------------------------------------- /qtmodern/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.0' 2 | """ str: Package version. """ 3 | -------------------------------------------------------------------------------- /qtmodern/_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os.path import join, dirname, abspath 3 | import qtpy 4 | import platform 5 | 6 | QT_VERSION = tuple(int(v) for v in qtpy.QT_VERSION.split('.')) 7 | """ tuple: Qt version. """ 8 | 9 | PLATFORM = platform.system() 10 | 11 | 12 | def resource_path(relative_path): 13 | if hasattr(sys, '_MEIPASS'): 14 | return join(sys._MEIPASS, dirname(abspath(__file__)), relative_path) 15 | return join(dirname(abspath(__file__)), relative_path) 16 | -------------------------------------------------------------------------------- /qtmodern/resources/frameless.qss: -------------------------------------------------------------------------------- 1 | #windowFrame { 2 | border-radius: 5px 5px 5px 5px; 3 | background-color: palette(Window); 4 | } 5 | 6 | #titleBar { 7 | border: 0px none palette(base); 8 | border-top-left-radius: 5px; 9 | border-top-right-radius: 5px; 10 | background-color: palette(Window); 11 | height: 24px; 12 | } 13 | 14 | #btnClose, #btnRestore, #btnMaximize, #btnMinimize { 15 | min-width: 14px; 16 | min-height: 14px; 17 | max-width: 14px; 18 | max-height: 14px; 19 | border-radius: 7px; 20 | margin: 4px; 21 | } 22 | 23 | #btnRestore, #btnMaximize { 24 | background-color: hsv(123, 204, 198); 25 | } 26 | 27 | #btnRestore::hover, #btnMaximize::hover { 28 | background-color: hsv(123, 204, 148); 29 | } 30 | 31 | #btnRestore::pressed, #btnMaximize::pressed { 32 | background-color: hsv(123, 204, 98); 33 | } 34 | 35 | #btnMinimize { 36 | background-color: hsv(38, 218, 253); 37 | } 38 | 39 | #btnMinimize::hover { 40 | background-color: hsv(38, 218, 203); 41 | } 42 | 43 | #btnMinimize::pressed { 44 | background-color: hsv(38, 218, 153); 45 | } 46 | 47 | #btnClose { 48 | background-color: hsv(0, 182, 252); 49 | } 50 | 51 | #btnClose::hover { 52 | background-color: hsv(0, 182, 202); 53 | } 54 | 55 | #btnClose::pressed { 56 | background-color: hsv(0, 182, 152); 57 | } 58 | 59 | #btnClose::disabled, #btnRestore::disabled, #btnMaximize::disabled, #btnMinimize::disabled { 60 | background-color: palette(midlight); 61 | } 62 | -------------------------------------------------------------------------------- /qtmodern/resources/style.qss: -------------------------------------------------------------------------------- 1 | /* 2 | * QGroupBox 3 | */ 4 | 5 | QGroupBox { 6 | background-color: palette(alternate-base); 7 | border: 1px solid palette(midlight); 8 | margin-top: 25px; 9 | } 10 | 11 | QGroupBox::title { 12 | background-color: transparent; 13 | } 14 | 15 | /* 16 | * QToolBar 17 | */ 18 | 19 | QToolBar { 20 | border: none; 21 | } 22 | 23 | /* 24 | * QTabBar 25 | */ 26 | 27 | QTabBar{ 28 | background-color: transparent; 29 | } 30 | 31 | QTabBar::tab{ 32 | padding: 4px 6px; 33 | background-color: transparent; 34 | border-bottom: 2px solid transparent; 35 | } 36 | 37 | QTabBar::tab:selected, QTabBar::tab:hover { 38 | color: palette(text); 39 | border-bottom: 2px solid palette(highlight); 40 | } 41 | 42 | QTabBar::tab:selected:disabled { 43 | border-bottom: 2px solid palette(light); 44 | } 45 | 46 | /* 47 | * QScrollBar 48 | */ 49 | 50 | QScrollBar:vertical { 51 | background: palette(base); 52 | border-top-right-radius: 2px; 53 | border-bottom-right-radius: 2px; 54 | width: 16px; 55 | margin: 0px; 56 | } 57 | 58 | QScrollBar::handle:vertical { 59 | background-color: palette(midlight); 60 | border-radius: 2px; 61 | min-height: 20px; 62 | margin: 2px 4px 2px 4px; 63 | } 64 | 65 | QScrollBar::handle:vertical:hover, QScrollBar::handle:horizontal:hover, QScrollBar::handle:vertical:pressed, QScrollBar::handle:horizontal:pressed { 66 | background-color:palette(highlight); 67 | } 68 | 69 | QScrollBar::add-line:vertical { 70 | background: none; 71 | height: 0px; 72 | subcontrol-position: right; 73 | subcontrol-origin: margin; 74 | } 75 | 76 | QScrollBar::sub-line:vertical { 77 | background: none; 78 | height: 0px; 79 | subcontrol-position: left; 80 | subcontrol-origin: margin; 81 | } 82 | 83 | QScrollBar:horizontal{ 84 | background: palette(base); 85 | height: 16px; 86 | margin: 0px; 87 | } 88 | 89 | QScrollBar::handle:horizontal { 90 | background-color: palette(midlight); 91 | border-radius: 2px; 92 | min-width: 20px; 93 | margin: 4px 2px 4px 2px; 94 | } 95 | 96 | 97 | QScrollBar::add-line:horizontal { 98 | background: none; 99 | width: 0px; 100 | subcontrol-position: bottom; 101 | subcontrol-origin: margin; 102 | } 103 | 104 | QScrollBar::sub-line:horizontal { 105 | background: none; 106 | width: 0px; 107 | subcontrol-position: top; 108 | subcontrol-origin: margin; 109 | } 110 | 111 | /* 112 | * QScrollArea 113 | */ 114 | 115 | QScrollArea { 116 | border-style: none; 117 | } 118 | 119 | QScrollArea > QWidget > QWidget { 120 | background-color: palette(alternate-base); 121 | } 122 | 123 | /* 124 | * QSlider 125 | */ 126 | 127 | QSlider::handle:horizontal { 128 | border-radius: 5px; 129 | background-color: palette(light); 130 | max-height: 20px; 131 | } 132 | 133 | QSlider::add-page:horizontal { 134 | background: palette(base); 135 | } 136 | 137 | QSlider::sub-page:horizontal { 138 | background: palette(highlight); 139 | } 140 | 141 | QSlider::sub-page:horizontal:disabled { 142 | background-color: palette(light); 143 | } 144 | 145 | QTableView { 146 | background-color: palette(link-visited); 147 | alternate-background-color: palette(midlight); 148 | } 149 | -------------------------------------------------------------------------------- /qtmodern/styles.py: -------------------------------------------------------------------------------- 1 | from qtpy.QtGui import QPalette, QColor 2 | from ._utils import QT_VERSION, resource_path 3 | 4 | _STYLESHEET = resource_path('resources/style.qss') 5 | """ str: Main stylesheet. """ 6 | 7 | 8 | def _apply_base_theme(app): 9 | """ Apply base theme to the application. 10 | 11 | Args: 12 | app (QApplication): QApplication instance. 13 | """ 14 | 15 | if QT_VERSION < (5,): 16 | app.setStyle('plastique') 17 | else: 18 | app.setStyle('Fusion') 19 | 20 | with open(_STYLESHEET) as stylesheet: 21 | app.setStyleSheet(stylesheet.read()) 22 | 23 | 24 | def dark(app): 25 | """ Apply Dark Theme to the Qt application instance. 26 | 27 | Args: 28 | app (QApplication): QApplication instance. 29 | """ 30 | 31 | darkPalette = QPalette() 32 | 33 | # base 34 | darkPalette.setColor(QPalette.WindowText, QColor(180, 180, 180)) 35 | darkPalette.setColor(QPalette.Button, QColor(53, 53, 53)) 36 | darkPalette.setColor(QPalette.Light, QColor(180, 180, 180)) 37 | darkPalette.setColor(QPalette.Midlight, QColor(90, 90, 90)) 38 | darkPalette.setColor(QPalette.Dark, QColor(35, 35, 35)) 39 | darkPalette.setColor(QPalette.Text, QColor(180, 180, 180)) 40 | darkPalette.setColor(QPalette.BrightText, QColor(180, 180, 180)) 41 | darkPalette.setColor(QPalette.ButtonText, QColor(180, 180, 180)) 42 | darkPalette.setColor(QPalette.Base, QColor(42, 42, 42)) 43 | darkPalette.setColor(QPalette.Window, QColor(53, 53, 53)) 44 | darkPalette.setColor(QPalette.Shadow, QColor(20, 20, 20)) 45 | darkPalette.setColor(QPalette.Highlight, QColor(42, 130, 218)) 46 | darkPalette.setColor(QPalette.HighlightedText, QColor(180, 180, 180)) 47 | darkPalette.setColor(QPalette.Link, QColor(56, 252, 196)) 48 | darkPalette.setColor(QPalette.AlternateBase, QColor(66, 66, 66)) 49 | darkPalette.setColor(QPalette.ToolTipBase, QColor(53, 53, 53)) 50 | darkPalette.setColor(QPalette.ToolTipText, QColor(180, 180, 180)) 51 | darkPalette.setColor(QPalette.LinkVisited, QColor(80, 80, 80)) 52 | 53 | # disabled 54 | darkPalette.setColor(QPalette.Disabled, QPalette.WindowText, 55 | QColor(127, 127, 127)) 56 | darkPalette.setColor(QPalette.Disabled, QPalette.Text, 57 | QColor(127, 127, 127)) 58 | darkPalette.setColor(QPalette.Disabled, QPalette.ButtonText, 59 | QColor(127, 127, 127)) 60 | darkPalette.setColor(QPalette.Disabled, QPalette.Highlight, 61 | QColor(80, 80, 80)) 62 | darkPalette.setColor(QPalette.Disabled, QPalette.HighlightedText, 63 | QColor(127, 127, 127)) 64 | 65 | app.setPalette(darkPalette) 66 | 67 | _apply_base_theme(app) 68 | 69 | 70 | def light(app): 71 | """ Apply Light Theme to the Qt application instance. 72 | 73 | Args: 74 | app (QApplication): QApplication instance. 75 | """ 76 | 77 | lightPalette = QPalette() 78 | 79 | # base 80 | lightPalette.setColor(QPalette.WindowText, QColor(0, 0, 0)) 81 | lightPalette.setColor(QPalette.Button, QColor(240, 240, 240)) 82 | lightPalette.setColor(QPalette.Light, QColor(180, 180, 180)) 83 | lightPalette.setColor(QPalette.Midlight, QColor(200, 200, 200)) 84 | lightPalette.setColor(QPalette.Dark, QColor(225, 225, 225)) 85 | lightPalette.setColor(QPalette.Text, QColor(0, 0, 0)) 86 | lightPalette.setColor(QPalette.BrightText, QColor(0, 0, 0)) 87 | lightPalette.setColor(QPalette.ButtonText, QColor(0, 0, 0)) 88 | lightPalette.setColor(QPalette.Base, QColor(237, 237, 237)) 89 | lightPalette.setColor(QPalette.Window, QColor(240, 240, 240)) 90 | lightPalette.setColor(QPalette.Shadow, QColor(20, 20, 20)) 91 | lightPalette.setColor(QPalette.Highlight, QColor(76, 163, 224)) 92 | lightPalette.setColor(QPalette.HighlightedText, QColor(0, 0, 0)) 93 | lightPalette.setColor(QPalette.Link, QColor(0, 162, 232)) 94 | lightPalette.setColor(QPalette.AlternateBase, QColor(225, 225, 225)) 95 | lightPalette.setColor(QPalette.ToolTipBase, QColor(240, 240, 240)) 96 | lightPalette.setColor(QPalette.ToolTipText, QColor(0, 0, 0)) 97 | lightPalette.setColor(QPalette.LinkVisited, QColor(222, 222, 222)) 98 | 99 | # disabled 100 | lightPalette.setColor(QPalette.Disabled, QPalette.WindowText, 101 | QColor(115, 115, 115)) 102 | lightPalette.setColor(QPalette.Disabled, QPalette.Text, 103 | QColor(115, 115, 115)) 104 | lightPalette.setColor(QPalette.Disabled, QPalette.ButtonText, 105 | QColor(115, 115, 115)) 106 | lightPalette.setColor(QPalette.Disabled, QPalette.Highlight, 107 | QColor(190, 190, 190)) 108 | lightPalette.setColor(QPalette.Disabled, QPalette.HighlightedText, 109 | QColor(115, 115, 115)) 110 | 111 | app.setPalette(lightPalette) 112 | 113 | _apply_base_theme(app) 114 | 115 | -------------------------------------------------------------------------------- /qtmodern/windows.py: -------------------------------------------------------------------------------- 1 | from qtpy.QtCore import Qt, QMetaObject, Signal, Slot, QPoint 2 | from qtpy.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QToolButton, 3 | QLabel, QSizePolicy) 4 | from ._utils import QT_VERSION, PLATFORM, resource_path 5 | 6 | 7 | _FL_STYLESHEET = resource_path('resources/frameless.qss') 8 | """ str: Frameless window stylesheet. """ 9 | 10 | 11 | class WindowDragger(QWidget): 12 | """ Window dragger. 13 | 14 | Args: 15 | window (QWidget): Associated window. 16 | parent (QWidget, optional): Parent widget. 17 | """ 18 | 19 | doubleClicked = Signal() 20 | 21 | def __init__(self, window, parent=None): 22 | QWidget.__init__(self, parent) 23 | 24 | self._window = window 25 | self._mousePressed = False 26 | 27 | def mousePressEvent(self, event): 28 | self._mousePressed = True 29 | self._mousePos = event.globalPos() 30 | self._windowPos = self._window.pos() 31 | 32 | def mouseMoveEvent(self, event): 33 | if self._mousePressed: 34 | if self._window.windowState() == Qt.WindowMaximized and PLATFORM != 'Darwin': 35 | # restore the window firstly 36 | self._window.on_btnRestore_clicked() 37 | # move the window back on the mouse 38 | self._window.move(self._mousePos - QPoint( 39 | 0.5 * self.geometry().width(), 40 | 0.5 * self.geometry().height(), 41 | )) 42 | # refresh _windowPos 43 | self._windowPos = self._window.pos() 44 | self._window.move(self._windowPos + 45 | (event.globalPos() - self._mousePos)) 46 | 47 | def mouseReleaseEvent(self, event): 48 | self._mousePressed = False 49 | 50 | def mouseDoubleClickEvent(self, event): 51 | self.doubleClicked.emit() 52 | 53 | 54 | class ModernWindow(QWidget): 55 | """ Modern window. 56 | 57 | Args: 58 | w (QWidget): Main widget. 59 | parent (QWidget, optional): Parent widget. 60 | """ 61 | 62 | def __init__(self, w, parent=None): 63 | QWidget.__init__(self, parent) 64 | 65 | self._w = w 66 | self.setupUi() 67 | 68 | contentLayout = QHBoxLayout() 69 | contentLayout.setContentsMargins(0, 0, 0, 0) 70 | contentLayout.addWidget(w) 71 | 72 | self.windowContent.setLayout(contentLayout) 73 | 74 | self.setWindowTitle(w.windowTitle()) 75 | self.setGeometry(w.geometry()) 76 | 77 | # Adding attribute to clean up the parent window when the child is closed 78 | self._w.setAttribute(Qt.WA_DeleteOnClose, True) 79 | self._w.destroyed.connect(self.__child_was_closed) 80 | 81 | def setupUi(self): 82 | # create title bar, content 83 | self.vboxWindow = QVBoxLayout(self) 84 | self.vboxWindow.setContentsMargins(0, 0, 0, 0) 85 | 86 | self.windowFrame = QWidget(self) 87 | self.windowFrame.setObjectName('windowFrame') 88 | 89 | self.vboxFrame = QVBoxLayout(self.windowFrame) 90 | self.vboxFrame.setContentsMargins(0, 0, 0, 0) 91 | 92 | self.titleBar = WindowDragger(self, self.windowFrame) 93 | self.titleBar.setObjectName('titleBar') 94 | self.titleBar.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, 95 | QSizePolicy.Fixed)) 96 | 97 | self.hboxTitle = QHBoxLayout(self.titleBar) 98 | self.hboxTitle.setContentsMargins(0, 0, 0, 0) 99 | self.hboxTitle.setSpacing(0) 100 | 101 | self.lblTitle = QLabel('Title') 102 | self.lblTitle.setObjectName('lblTitle') 103 | self.lblTitle.setAlignment(Qt.AlignCenter) 104 | 105 | spButtons = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 106 | 107 | self.btnMinimize = QToolButton(self.titleBar) 108 | self.btnMinimize.setObjectName('btnMinimize') 109 | self.btnMinimize.setSizePolicy(spButtons) 110 | 111 | self.btnRestore = QToolButton(self.titleBar) 112 | self.btnRestore.setObjectName('btnRestore') 113 | self.btnRestore.setSizePolicy(spButtons) 114 | 115 | self.btnMaximize = QToolButton(self.titleBar) 116 | self.btnMaximize.setObjectName('btnMaximize') 117 | self.btnMaximize.setSizePolicy(spButtons) 118 | 119 | self.btnClose = QToolButton(self.titleBar) 120 | self.btnClose.setObjectName('btnClose') 121 | self.btnClose.setSizePolicy(spButtons) 122 | 123 | self.vboxFrame.addWidget(self.titleBar) 124 | 125 | self.windowContent = QWidget(self.windowFrame) 126 | self.vboxFrame.addWidget(self.windowContent) 127 | 128 | self.vboxWindow.addWidget(self.windowFrame) 129 | 130 | if PLATFORM == "Darwin": 131 | self.hboxTitle.addWidget(self.btnClose) 132 | self.hboxTitle.addWidget(self.btnMinimize) 133 | self.hboxTitle.addWidget(self.btnRestore) 134 | self.hboxTitle.addWidget(self.btnMaximize) 135 | self.hboxTitle.addWidget(self.lblTitle) 136 | else: 137 | self.hboxTitle.addWidget(self.lblTitle) 138 | self.hboxTitle.addWidget(self.btnMinimize) 139 | self.hboxTitle.addWidget(self.btnRestore) 140 | self.hboxTitle.addWidget(self.btnMaximize) 141 | self.hboxTitle.addWidget(self.btnClose) 142 | 143 | # set window flags 144 | self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint | Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint | 145 | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint) 146 | 147 | if QT_VERSION >= (5,): 148 | self.setAttribute(Qt.WA_TranslucentBackground) 149 | 150 | # set stylesheet 151 | with open(_FL_STYLESHEET) as stylesheet: 152 | self.setStyleSheet(stylesheet.read()) 153 | 154 | # automatically connect slots 155 | QMetaObject.connectSlotsByName(self) 156 | 157 | def __child_was_closed(self): 158 | self._w = None # The child was deleted, remove the reference to it and close the parent window 159 | self.close() 160 | 161 | def closeEvent(self, event): 162 | if not self._w: 163 | event.accept() 164 | else: 165 | self._w.close() 166 | event.setAccepted(self._w.isHidden()) 167 | 168 | def setWindowTitle(self, title): 169 | """ Set window title. 170 | 171 | Args: 172 | title (str): Title. 173 | """ 174 | 175 | super(ModernWindow, self).setWindowTitle(title) 176 | self.lblTitle.setText(title) 177 | 178 | def _setWindowButtonState(self, hint, state): 179 | btns = { 180 | Qt.WindowCloseButtonHint: self.btnClose, 181 | Qt.WindowMinimizeButtonHint: self.btnMinimize, 182 | Qt.WindowMaximizeButtonHint: self.btnMaximize 183 | } 184 | button = btns.get(hint) 185 | 186 | maximized = bool(self.windowState() & Qt.WindowMaximized) 187 | 188 | if button == self.btnMaximize: # special rules for max/restore 189 | self.btnRestore.setEnabled(state) 190 | self.btnMaximize.setEnabled(state) 191 | 192 | if maximized: 193 | self.btnRestore.setVisible(state) 194 | self.btnMaximize.setVisible(False) 195 | else: 196 | self.btnMaximize.setVisible(state) 197 | self.btnRestore.setVisible(False) 198 | else: 199 | button.setEnabled(state) 200 | 201 | allButtons = [self.btnClose, self.btnMinimize, self.btnMaximize, self.btnRestore] 202 | if True in [b.isEnabled() for b in allButtons]: 203 | for b in allButtons: 204 | b.setVisible(True) 205 | if maximized: 206 | self.btnMaximize.setVisible(False) 207 | else: 208 | self.btnRestore.setVisible(False) 209 | self.lblTitle.setContentsMargins(0, 0, 0, 0) 210 | else: 211 | for b in allButtons: 212 | b.setVisible(False) 213 | self.lblTitle.setContentsMargins(0, 2, 0, 0) 214 | 215 | def setWindowFlag(self, Qt_WindowType, on=True): 216 | buttonHints = [Qt.WindowCloseButtonHint, Qt.WindowMinimizeButtonHint, Qt.WindowMaximizeButtonHint] 217 | 218 | if Qt_WindowType in buttonHints: 219 | self._setWindowButtonState(Qt_WindowType, on) 220 | else: 221 | QWidget.setWindowFlag(self, Qt_WindowType, on) 222 | 223 | def setWindowFlags(self, Qt_WindowFlags): 224 | buttonHints = [Qt.WindowCloseButtonHint, Qt.WindowMinimizeButtonHint, Qt.WindowMaximizeButtonHint] 225 | for hint in buttonHints: 226 | self._setWindowButtonState(hint, bool(Qt_WindowFlags & hint)) 227 | 228 | QWidget.setWindowFlags(self, Qt_WindowFlags) 229 | 230 | @Slot() 231 | def on_btnMinimize_clicked(self): 232 | self.setWindowState(Qt.WindowMinimized) 233 | 234 | @Slot() 235 | def on_btnRestore_clicked(self): 236 | if self.btnMaximize.isEnabled() or self.btnRestore.isEnabled(): 237 | self.btnRestore.setVisible(False) 238 | self.btnRestore.setEnabled(False) 239 | self.btnMaximize.setVisible(True) 240 | self.btnMaximize.setEnabled(True) 241 | 242 | self.setWindowState(Qt.WindowNoState) 243 | 244 | @Slot() 245 | def on_btnMaximize_clicked(self): 246 | if self.btnMaximize.isEnabled() or self.btnRestore.isEnabled(): 247 | self.btnRestore.setVisible(True) 248 | self.btnRestore.setEnabled(True) 249 | self.btnMaximize.setVisible(False) 250 | self.btnMaximize.setEnabled(False) 251 | 252 | self.setWindowState(Qt.WindowMaximized) 253 | 254 | @Slot() 255 | def on_btnClose_clicked(self): 256 | self.close() 257 | 258 | @Slot() 259 | def on_titleBar_doubleClicked(self): 260 | if not bool(self.windowState() & Qt.WindowMaximized): 261 | self.on_btnMaximize_clicked() 262 | else: 263 | self.on_btnRestore_clicked() 264 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | qtpy>=1.3.1 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import qtmodern 4 | from setuptools import setup 5 | 6 | _version = qtmodern.__version__ 7 | 8 | setup(name='qtmodern', 9 | version=_version, 10 | packages=['qtmodern'], 11 | description='Qt Widgets Modern User Interface', 12 | long_description=open('README.rst').read(), 13 | author='Gerard Marull-Paretas', 14 | author_email='gerardmarull@gmail.com', 15 | url='https://www.github.com/gmarull/qtmodern', 16 | license='MIT', 17 | classifiers=[ 18 | 'Development Status :: 3 - Alpha', 19 | 'Intended Audience :: Developers', 20 | 'License :: OSI Approved :: MIT License', 21 | 'Operating System :: OS Independent', 22 | 'Programming Language :: Python', 23 | 'Topic :: Software Development :: User Interfaces' 24 | ], 25 | package_data={ 26 | 'qtmodern': ['resources/*'] 27 | }, 28 | install_requires=['qtpy>=1.3.1']) 29 | --------------------------------------------------------------------------------