본문 바로가기
Mastering-GUI-Programming

Styling Qt Applications

by 자동매매 2023. 3. 8.

It is easy to appreciate the clean, native look that Qt effortlessly provides by default. But for less business-like applications, plain gray widgets and bog-standard fonts don't always set the right tone. Even the drabbest utility or data entry application occasionally benefits from the addition of icons or the judicious tweaking of fonts to enhance usability. Fortunately, Qt's flexibility allows us to take the look and feel of our application into our own hands.

In this chapter, we'll cover the following topics:

  • Using fonts, images, and icons
  • Configuring colors, style sheets, and styles
  • Creating animations

Technical requirements

In this chapter, you'll need all the requirements listed in Chapter 1, Getting Started with PyQt, and the Qt application template from Chapter 4, Building Applications with QMainWindow.

Additionally, you may require PNG, JPEG, or GIF image files to work with; you can use those included in the example code at https://github.com/PacktPublishing/Mastering- GUI-Programming-with-Python/tree/master/Chapter06.

Check out the following video to see the code in action: http://bit.ly/2M5OJj6

Styling Qt Applications Chapter 144

*

Using fonts, images, and icons

We'll begin styling our Qt application by customizing the application's fonts, displaying some static images, and including dynamic icons. However, before we can do this, we'll need to create a graphical user interface (GUI) that we can work with. We'll create a game lobby dialog, which will be used for logging into an imaginary multiplayer game called Fight Fighter.

To do this, open a fresh copy of your application template and add the following GUI code to MainWindow.__init__():

self.setWindowTitle('Fight Fighter Game Lobby') cx_form = qtw.QWidget() self.setCentralWidget(cx_form) cx_form.setLayout(qtw.QFormLayout())

heading = qtw.QLabel("Fight Fighter!") cx_form.layout().addRow(heading)

inputs = {

'Server': qtw.QLineEdit(),

'Name': qtw.QLineEdit(),

'Password': qtw.QLineEdit( echoMode=qtw.QLineEdit.Password),

'Team': qtw.QComboBox(),

'Ready': qtw.QCheckBox('Check when ready')

}

teams = ('Crimson Sharks', 'Shadow Hawks',

'Night Terrors', 'Blue Crew') inputs['Team'].addItems(teams)

for label, widget in inputs.items():

cx_form.layout().addRow(label, widget)

self.submit = qtw.QPushButton(

'Connect',

clicked=lambda: qtw.QMessageBox.information(

None, 'Connecting', 'Prepare for Battle!'))

self.reset = qtw.QPushButton('Cancel', clicked=self.close) cx_form.layout().addRow(self.submit, self.reset)

This is fairly standard Qt GUI code that you should be familiar with by now; we're saving a few lines of code by putting our inputs in a dict object and adding them to the layout in a loop, but otherwise, it's relatively straightforward. Depending on your OS and theme settings, the dialog box probably looks something like the following screenshot:

As you can see, it's a nice form but it's a bit bland. So, let's explore whether we can improve the style.

Setting a font

The first thing we'll tackle is the font. Every QWidget class has a font property, which we can either set in the constructor or by using the setFont() accessor. The value of font must be a QtGui.QFont object.

Here is how you can create and use a QFont object:

heading_font = qtg.QFont('Impact', 32, qtg.QFont.Bold) heading_font.setStretch(qtg.QFont.ExtraExpanded) heading.setFont(heading_font)

A QFont object contains all the attributes that describe the way text will be drawn to the screen. The constructor can take any of the following arguments:

  • A string indicating the font family
  • A float or integer indicating the point size
  • A QtGui.QFont.FontWeight constant indicating the weight
  • A Boolean indicating whether the font should be italic

The remaining aspects of the font, such as the stretch property, can be configured using keyword arguments or accessor methods. We can also create a QFont object with no arguments and configure it programmatically, as follows:

label_font = qtg.QFont() label_font.setFamily('Impact') label_font.setPointSize(14) label_font.setWeight(qtg.QFont.DemiBold) label_font.setStyle(qtg.QFont.StyleItalic)

for inp in inputs.values(): cx_form.layout().labelForField(inp).setFont(label_font)

Setting a font on a widget affects not only the widget but also all its child widgets. Therefore, we could configure the font for the entire form by setting it on cx_form rather than setting it on individual widgets.

Dealing with missing fonts

Now, if all platforms and operating systems (OSes) shipped with an infinite array of identically named fonts, this would be all you'd need to know about QFont. Unfortunately, that isn't the case. Most systems ship with only a handful of fonts built-in and only a few of these are universal across platforms or even different versions of a platform. Therefore, Qt has a fallback mechanism for dealing with missing fonts.

For example, suppose that we ask Qt to use a nonexistent font family, as follows:

button_font = qtg.QFont(

'Totally Nonexistant Font Family XYZ', 15.233)

Qt will not throw an error at this call or even register a warning. Instead, after not finding the font family requested, it will fall back to its defaultFamily property, which utilizes

the default font set in the OS or desktop environment.

The QFont object won't actually tell us that this has happened; if you query it for information, it will only tell you what was configured:

print(f'Font is {button_font.family()}')

  • Prints: "Font is Totally Nonexistent Font Family XYZ"

To discover what font settings are actually being used, we need to pass our QFont object to a QFontInfo object:

actual_font = qtg.QFontInfo(button_font).family() print(f'Actual font used is {actual_font}')

If you run the script, you'll see that, more than likely, your default screen font is actually being used here:

$ python game_lobby.py

Font is Totally Nonexistent Font Family XYZ Actual font used is Bitstream Vera Sans

While this ensures that users won't be left without any text in the window, it would be nice if we could give Qt a better idea of what sort of font it should use.

We can do this by setting the font's styleHint and styleStrategy properties, as follows:

button_font.setStyleHint(qtg.QFont.Fantasy) button_font.setStyleStrategy(

qtg.QFont.PreferAntialias | qtg.QFont.PreferQuality

)

styleHint suggests a general category for Qt to fall back on, which, in this case, is the Fantasy category. Other options here include SansSerif, Serif, TypeWriter, Decorative, Monospace, and Cursive. What these options correspond to is dependent on the OS and desktop environment configuration.

The styleStrategy property informs Qt of more technical preferences related to the capabilities of the chosen font, such as anti-aliasing, OpenGL compatibility, and whether the size will be matched exactly or rounded to the nearest non-scaled size. The complete list of strategy options can be found at https://doc.qt.io/qt-5/qfont.html#StyleStrategy-

enum.

After setting these properties, check the font again to see whether anything has changed:

actual_font = qtg.QFontInfo(button_font)

print(f'Actual font used is {actual_font.family()}' f' {actual_font.pointSize()}') self.submit.setFont(button_font) self.cancel.setFont(button_font)

Depending on your system's configuration, you should see different results from before:

$ python game_lobby.py Actual font used is Impact 15

On this system, Fantasy has been interpreted to mean Impact, and the PreferQuality strategy flag has forced the initially odd 15.233 point size to be a nice round 15.

At this point, depending on the fonts available on your system, your application should look as follows:

Fonts can also be bundled with the application; see the Using Qt resource files section in this chapter.

Adding images

Qt offers a number of classes related to the use of images in an application, but, for simply displaying a picture in your GUI, the most appropriate is QPixmap. QPixmap is a display- optimized image class, which can load many common image formats including PNG, BMP, GIF, and JPEG.

To create one, we simply need to pass QPixmap a path to an image file:

logo = qtg.QPixmap('logo.png')

Once loaded, a QPixmap object can be displayed in a QLabel or QButton object, as follows:

heading.setPixmap(logo)

Note that labels can only display a string or a pixmap, but not both.

Being optimized for display, the QPixmap objects offer only minimal editing functionality; however, we can do simple transformations such as scaling:

if logo.width() > 400:

logo = logo.scaledToWidth(

400, qtc.Qt.SmoothTransformation)

In this example, we've used the pixmap's scaledToWidth() method to restrict the logo's width to 400 pixels using a smooth transformation algorithm.

The reason why QPixmap objects are so limited is that they are actually stored in the display server's memory. The QImage class is similar but stores data in application memory, so that it can be edited more extensively. We'll explore this class more in Chapter 12, Creating 2D Graphics with QPainter.

QPixmap also offers the handy capability to generate simple colored rectangles, as follows:

go_pixmap = qtg.QPixmap(qtc.QSize(32, 32))

stop_pixmap = qtg.QPixmap(qtc.QSize(32, 32)) go_pixmap.fill(qtg.QColor('green')) stop_pixmap.fill(qtg.QColor('red'))

By specifying a size in the constructor and using the fill() method, we can create a

simple, colored rectangle pixmap. This is useful for displaying color swatches or to use as a quick-and-dirty image stand-in.

Using icons

Now consider an icon on a toolbar or in a program menu. When the menu item is disabled, you expect the icon to be grayed out in some way. Likewise, if a user hovers over the button or item using a mouse cursor, you might expect it to be highlighted. To encapsulate this type of state-dependent image display, Qt provides the QIcon class. A QIcon object contains a collection of pixmaps that are each mapped to a widget state.

Here is how you can create a QIcon object:

connect_icon = qtg.QIcon()

connect_icon.addPixmap(go_pixmap, qtg.QIcon.Active)

connect_icon.addPixmap(stop_pixmap, qtg.QIcon.Disabled)

After creating the icon object, we use its addPixmap() method to assign a QPixmap object to a widget state. These states include Normal, Active, Disabled, and Selected.

The connect_icon icon will now be a red square when disabled, or a green square when enabled. Let's add it to our submit button and add some logic to toggle the button's status:

self.submit.setIcon(connect_icon) self.submit.setDisabled(True) inputs['Server'].textChanged.connect(

lambda x: self.submit.setDisabled(x == '') )

If you run the script at this point, you'll see that the red square appears in the submit button until the Server field contains data, at which point it automatically switches to green.

Notice that we don't have to tell the icon object itself to switch states; once assigned to the widget, it tracks any changes in the widget's state.

Icons can be used with the QPushButton, QToolButton, and QAction objects; the QComboBox, QListView, QTableView, and QTreeView items; and most other places where you might reasonably expect to have an icon.

Using Qt resource files

A significant problem with using image files in a program is making sure the program can find them at runtime. Paths passed into a QPixmap constructor or a QIcon constructor are interpreted as absolute (that is, if they begin with a drive letter or path separator), or as relative to the current working directory (which you cannot control). For example, try running your script from somewhere other than the code directory, as follows:

$ cd ..

$ python ch05/game_lobby.py

You'll find that your images are all missing! QPixmap does not complain when it cannot find a file, it just doesn't show anything. Without an absolute path to the images, you'll only be able to find them if the script is run from the exact directory to which your paths are relative.

Unfortunately, specifying absolute paths means that your program will only work from one location on the filesystem, which is a major problem if you plan to distribute it to multiple platforms.

PyQt offers us a solution to this problem in the form of a PyQt Resource file, which we can create using the PyQt resource compiler tool. The basic procedure is as follows:

  1. Write an XML-format Qt Resource Collection file (.qrc) containing the paths of all the files that we want to include
  2. Run the pyrcc5 tool to serialize and compress these files into data contained in a Python module
  3. Import the resulting Python module into our application script
  4. Now we can reference our resources using a special syntax

Let's step through this process—suppose that we have some team badges in the form of PNG files that we want to include in our program. Our first step is to create the resources.qrc file, which looks like the following code block:

crimson_sharks.png shadow_hawks.png night_terrors.png

blue_crew2.png

We've placed this file in the same directory as the image files listed in the script. Note that we've added a prefix value of teams. Prefixes allow you to organize resources into categories. Additionally, notice that the last file has an alias specified. In our program, we can use this alias rather than the actual name of the file to access this resource.

Now, in the command line, we'll run pyrcc5, as follows:

$ pyrcc5 -o resources.py resources.qrc

The syntax here is pyrcc5 -o outputFile.py inputFile.qrc. This command should generate a Python file containing your resource data. If you take a moment to open the file and examine it, you'll find it's mostly just a large bytes object assigned to the qt_resource_data variable.

Back in our main script, we just need to import this file in the same way as any other Python file:

import resources

The file doesn't have to be called resources.py; in fact, any name will suffice. You just need to import it, and the code in the file will make sure that the resources are available to Qt.

Now that the resource file is imported, we can specify pixmap paths using the resource syntax:

inputs['Team'].setItemIcon(

0, qtg.QIcon(':/teams/crimson_sharks.png')) inputs['Team'].setItemIcon(

1, qtg.QIcon(':/teams/shadow_hawks.png')) inputs['Team'].setItemIcon(

2, qtg.QIcon(':/teams/night_terrors.png')) inputs['Team'].setItemIcon(

3, qtg.QIcon(':/teams/blue_crew.png'))

Essentially, the syntax is :/prefix/file_name_or_alias.extension.

Because our data is stored in a Python file, we can place it inside a Python library and it will use Python's standard import resolution rules to locate the file.

Qt resource files and fonts

Resource files aren't limited to images; in fact, they can be used to include just about any kind of binary, including font files. For example, suppose that we want to include our favorite font in the program to ensure that it looks right on all platforms.

Just as with images, we start by including the font file in the .qrc file:

crimson_sharks.pngshadow_hawks.pngnight_terrors.pngblue_crew.png

LiberationSans-Regular.ttf

Here, we've added a prefix of fonts and included a reference to the LiberationSans- Regular.ttf file. After running pyrcc5 against this file, the font is bundled into our resources.py file.

To use this font in the code, we start by adding it to the font database, as follows:

libsans_id = qtg.QFontDatabase.addApplicationFont( ':/fonts/LiberationSans-Regular.ttf')

QFontDatabase.addApplicationFont() inserts the passed font file into the application's font database and returns an ID number. We can then use that ID number to determine the font's family string; this can be passed to QFont, as follows:

family = qtg.QFontDatabase.applicationFontFamilies(libsans_id)[0] libsans = qtg.QFont(family) inputs['Team'].setFont(libsans)

Make sure to check the license on your font before distributing it with your application! Remember that not all fonts are free to redistribute.

Configuring colors, style sheets, and styles

Fonts and icons have improved the look of our form, but now it's time to ditch those institutional gray tones and replace them with some color. In this section, we're going to look at three different approaches that Qt offers for customizing application colors: manipulating the palette, using style sheets, and overriding the application style.

 

 

Customizing colors with palettes

A palette, represented by the QPalette class, is a collection of colors and brushes that are mapped to color roles and color groups. 

Let's unpack that statement: 

  • Here, color is a literal color value, represented by a QColor object
  • A brush combines a particular color with a style, such as a pattern, gradient, or texture, and is represented by a QBrush class
  • A color role represents the way a widget uses the color, such as in the foreground, in the background, or in the border
  • The color group refers to the interaction state of the widget; it can be Normal, Active, Disabled, or Inactive

 위젯이 화면에 그려지면 Qt의 페인팅 시스템은 팔레트를 참조하여 위젯의 각 부분을 렌더링하는 데 사용되는 색상과 브러시를 결정합니다. 이를 사용자 정의하기 위해 자체 팔레트를 만들어 위젯에 할당 할 수 있습니다.

 

To begin, we need to get a QPalette object, as follows:

app = qtw.QApplication.instance()
palette = app.palette()

 

QPa lette 객체를 직접 만들 수도 있지만  , Qt 문서에서는  실행중인 QApplic a tion 인스턴스에서 palette() 를 호출  하여 현재 구성된 스타일에 대한 팔레트 사본을 검색 할 것을 권장합니다.

You can always retrieve a copy of your QApplication object by calling QApplication.instance().

Now that we have the palette, let's start overriding some of the rules:

 

    palette.setColor(qtg.QPalette.Button, qtg.QColor('#333'))

    palette.setColor(qtg.QPalette.ButtonText, qtg.QColor('#3F3'))

 

QtGui.QPalette.Button and QtGui.QPalette.ButtonText are color role constants and, as you might guess, they represent the background and foreground colors, respectively, of all the Qt button classes. We're overriding them with new colors. 

To override the color for a particular button state, we need to pass in a color group constant as the first argument:

 

    palette.setColor( qtg.QPalette.Disabled, qtg.QPalette.ButtonText, qtg.QColor('#F88'))

    palette.setColor(qtg.QPalette.Disabled, qtg.QPalette.Button, qtg.QColor('#888'))

 

In this case, we're changing the colors used when a button is in the Disabled state. 

To apply this new palette, we have to assign it to a widget, as follows:

 

    self.submit.setPalette(palette)

    self.cancel.setPalette(palette)

 

setPalette() assigns the provided palette to the widget and all the child widgets as well. So, rather than assigning this to individual widgets, we could create a single palette and assign it to our QMainWindow class to apply it to all objects.

 

Working with QBrush objects

If we want something fancier than a solid color, then we can use a QBrush object. Brushes are capable of filling colors in patterns, gradients, or textures (that is, image-based patterns). 

For example, let's create a brush that paints a white stipple fill: 

dotted_brush = qtg.QBrush( qtg.QColor('white'), qtc.Qt.Dense2Pattern)

Dense2Pattern is one of 15 patterns available. (You can refer to https://doc.qt.io/qt-5/qt.html#BrushStyle-enum for the full list.) Most of these are varying degrees of stippling, cross-hatching, or alternating line patterns. 

Patterns have their uses, but gradient-based brushes are perhaps more interesting for modern styling. However, creating one is a little more involved, as shown in the following code: 

gradient = qtg.QLinearGradient(0, 0, self.width(), self.height()) 
gradient.setColorAt(0, qtg.QColor('navy')) 
gradient.setColorAt(0.5, qtg.QColor('darkred')) 
gradient.setColorAt(1, qtg.QColor('orange'))
gradient_brush = qtg.QBrush(gradient)

 To use a gradient in a brush, we first have to create a gradient object. Here, we've created a QLinearGradient object, which implements a basic linear gradient. The arguments are the starting and ending coordinates for the gradient, which we've specified as the top-left (0, 0), and the bottom-right (width, height) of the main window. 

Qt also offers the QRadialGradient and QConicalGradient classes for additional gradient options. 

After creating the object, we then specify color stops using setColorAt(). The first argument is a float value between 0 and 1 that specifies the percentage between the start and finish, and the second argument is the QColor object that the gradient should be at that point.

 

After creating the gradient, we pass it to the QBrush constructor to create a brush that paints with our gradient.

We can now apply our brushes to a palette using the setBrush() method, as follows: 

window_palette = app.palette() 
window_palette.setBrush(qtg.QPalette.Window, gradient_brush) 
window_palette.setBrush(qtg.QPalette.Active, qtg.QPalette.WindowText, dotted_brush) 
self.setPalette(window_palette)

Just as with QPalette.setColor(), we can assign our brush with or without specifying a specific color group. In this case, our gradient brush will be used to paint the main window regardless of its state, but our dotted brush will only be used when the widget is active (that is, the currently active window). 

 

Customizing the appearance with Qt Style Sheets (QSS)

웹 기술을 사용해 본 개발자의 경우 색상표, 브러시 및 색 개체를 사용하여 응용 프로그램의 스타일을 지정하는 것이 장황하고 직관적이지 않은 것처럼 보일 수 있습니다. 다행히 Qt는 웹 개발에 사용되는 CSS  (Cascading  Style Sheets)와 매우 유사한 QSS라는 대안을 제공합니다. 위젯에 몇 가지 간단한 변경 사항을 적용하는 쉬운 방법입니다. 

You can use QSS as follows:

 

        stylesheet = """

        QMainWindow {background-color: blue; }

        QWidget {background-color: transparent; color: #3F3;}

        QLineEdit, QComboBox, QCheckBox { font-size: 16pt;}

        """

        self.setStyleSheet(stylesheet)

 

여기서 스타일 시트는 스타일 지시문을 포함하는 문자열일 뿐이며 위젯의 styleSheet 속성에 할당할 수 있습니다. 

CSS로 작업 한 모든 사람에게 친숙한 구문은 다음과 같습니다.

WidgetClass {property-name: value; property-name2: value2;}

이 시점에서 프로그램을 실행하면 실망스럽게도 시스템 테마에 따라 다음 스크린 샷과 같이 보일 수 있습니다.

 

 

여기서 인터페이스는 텍스트와 이미지를 제외하고 대부분 검은 색으로 변했습니다. 특히 버튼과 확인란은 배경과 구별 할 수 없습니다. 그렇다면 왜 이런 일이 일어 났습니까? 

위젯 클래스에 QSS 스타일을 추가하면 스타일 변경이 모든 하위 클래스로 전달됩니다. QWi d g et  의 스타일을 지정했기 때문에  다른 모든  Q Widget 파생 클래스(:  Q C h eckbox QP u shButton)가 이 스타일을 계승했습니다. 

다음과 같이 해당 하위 클래스의 스타일을 재정의하여 이 문제를 해결해 보겠습니다.

 

        stylesheet += """

        QPushButton {background-color: #333; }

        QCheckBox::indicator:unchecked { border: 1px solid silver; background-color: darkred;}

        QCheckBox::indicator:checked { border: 1px solid silver; background-color: #3F3;} """

 

        self.setStyleSheet(stylesheet)

 

CSS와 마찬가지로 보다 구체적인 클래스에 스타일을 적용하면 보다 일반적인 경우를 재정의합니다. 예를 들어, QPushButton 배경색은 QWidget 배경색을 재정의합니다. 

QCheckBox와 함께 콜론을 사용하면  QSS의 이중 콜론을 사용하여 위젯의 하위 요소를 참조할 수 있습니다. 이 경우, 이것은 QCheckBox 클래스의 지시자 부분입니다  (라벨 부분과 반대). 또한 단일 콜론을 사용하여 위젯 상태를 참조할 수 있는데, 이 경우 확인란이 선택되었는지 여부에 따라 다른 스타일을 설정하므로 위젯 상태를 참조할 수도 있습니다. 

변경 사항을 특정 클래스로만 제한하고 하위 클래스로 제한하지 않으려면 마침표(. )을 다음과 같이 이름에 추가합니다.

 

stylesheet += """ .QWidget {background: url(tile.png); }"""

 

앞의 예제에서는 QSS에서 이미지를 사용하는 방법도 보여 줍니다. CSS에서와 마찬가지로 url() 함수로 래핑된 파일 경로를 제공할 수  있습니다. 

QSS also accepts resource paths if you've serialized your images with pyrcc5. 

 

위젯의 전체 클래스가 아닌 특정 위젯에 스타일을 적용하려는 경우 두 가지 방법이 있습니다. 

첫 번째 방법은  다음과 같이 objectName 속성을 사용하는 것입니다.

 

        self.button.setObjectName('SubmitButton')

        stylesheet += """#SubmitButton:disabled { background-color: #888; color: darkred;} """ 

 

스타일시트에서 개체 이름 앞에는 클래스 아닌 개체 이름으로 식별하기 위해 # 기호가 와야 합니다. 

개별 위젯에서 스타일을 설정하는 다른 방법은  다음과 같이 일부 스타일 시트 지시문과 함께 위젯의 set S tyleSheet() 메서드를 호출하는 것입니다.

 

        for inp in ('Server', 'Name', 'Password'):

            inp_widget = inputs[inp]

            inp_widget.setStyleSheet('background-color: black')

 

 

호출하는 위젯에 직접 스타일을 적용하려면 클래스 이름이나 개체 이름을 지정할 필요가 없습니다. 우리는 단순히 속성과 값을 전달할 수 있습니다. 

이러한 모든 변경 사항을 적용한 후 이제 응용 프로그램은 게임 GUI처럼 보입니다. 

 

The downside of QSS

보시다시피 QSS는 매우 강력한 스타일링 방법이며 웹 개발 작업을 한 모든 개발자가 액세스 할 수 있습니다. 그러나 몇 가지 단점이 있습니다. 

QSS는 팔레트 및 스타일 객체에 대한 추상화이며 실제 시스템으로 변환되어야 합니다. 이렇게 하면 큰 응용 프로그램의 경우 속도가 느려지고 검색 및 편집할 수 있는 기본 스타일시트가 없으므로  매번 처음부터 시작해야 합니다. 

이미 살펴본 것처럼 QSS는 클래스 계층 구조를 통해 상속되기 때문에 상위 수준 위젯에 적용될 때 예측할 수 없는 결과를 초래할 수 있습니다. 

마지막으로, QSS는 몇 가지 추가 또는 변경 사항이 있는 CSS 2.0의 적당한 하위 집합이며  CSS가 아닙니다. 따라서 전환, 애니메이션, 플렉스 박스 컨테이너, 상대 단위 및 기타 최신 CSS 기능이 완전히 없습니다. 따라서 웹 개발자는 기본 구문에 익숙할 수 있지만 제한된 옵션 세트는 실망스럽고 다양한 동작이 혼란스러울 수 있습니다. 

 

Customizing the appearance with QStyle

팔레트와 스타일 시트는 Qt 응용 프로그램의 모양을 사용자 정의하는 데 먼 길을 갈 수 있으며 대부분의 경우 이것이 필요한 전부입니다. Qt 응용 프로그램 모양의 핵심을 실제로 파헤 치려면 스타일 시스템을 이해해야합니다. 

Qt 응용 프로그램의 실행중인 모든 인스턴스에는 그래픽 시스템에 각 위젯 또는 GUI 구성 요소를 그리는 방법을 알려주는 단일 스타일이 있습니다. 스타일은 동적이고 플러그 가능하므로 OS 플랫폼마다 스타일이 다르며 사용자는 Qt 응용 프로그램에서 사용할 자체 Qt 스타일을 설치할 수 있습니다. 이것이 Qt 응용 프로그램이 다른 OS에서 기본 모양을 가질 수있는 방법입니다. 

Ch a pter 1,  PyQt 시작하기에서 우리는 QApplic a tion 이 생성될 때 s y s.a rgv 의 복사본을 전달해야  일부  Qt 관련 인수를 처리할 수 있다는  것을 배웠습니다. 그러한 인수 중 하나는 -style, 사용자가 Qt 응용 프로그램에 대한 사용자 정의 스타일을 설정할 수 있습니다. 

예를 들어 Chapter 3,  신호 슬롯이 있는 이벤트 처리에서 Windows 스타일을 사용하여 달력 응용 프로그램을 실행해 보겠습니다  . 

$ python3 calendar_app.py -style Windows 

이제  다음과 같이 Fusion 스타일을 사용하여 시도하십시오. 

$ python3 calendar_app.py -style Fusion 

특히 입력 컨트롤에서 모양의 차이를 확인합니다. 

Capitalization counts with styles; windows is not a valid style, whereas Windows is! 

The styles that are available on common OS platforms are shown in the following table: 

OS Styles
Windows 10 windowsvista, Windows, and Fusion
macOS macintosh, Windows, and Fusion
Ubuntu 18.04 Windows and Fusion

 

많은 Linux 배포판에서 패키지 저장소에서 추가 Qt 스타일을 사용할 수 있습니다. 현재 설치된 스타일 목록은 QtWidgets.QStyleFactory.keys()를 호출하여 얻을 수 있습니다. 

Styles can also be set inside the application itself. In order to retrieve a style class, we need to use the QStyleFactory class, as follows:

 

if __name__ == '__main__':

    app = qtw.QApplication(sys.argv)

    windows_style = qtw.QStyleFactory.create('Windows')

    app.setStyle(windows_style)

 

QStyleFactory.create() will attempt to find an installed style with the given name and return a QCommonStyle object; if the style requested is not found, then it will return None. The style object can then be used to set the style property of our QApplication object. (A value of None will just cause it to use the default.) 

If you plan to set a style inside the application, it's best to do it as early as possible before any widgets are drawn to avoid visual glitches. 

 

Customizing Qt styles

Qt 스타일을 만드는 것은 Qt의 위젯 및 페인팅 시스템에 대한 깊은 이해가 필요한 복잡한 프로세스이며, 이를 만들 필요가 있는 개발자는 거의 없습니다. 그러나 색상표 또는 스타일 시트를 조작하여 불가능한 몇 가지 작업을 수행하기 위해 실행 스타일의 일부 측면을 재정의할 수 있습니다. QtWidgets.QProxyStyle를 서브클래싱하여 이를 수행할 수 있습니다. 

A proxy style is an overlay that we can use to override methods of the actual style that's running. In this way, it doesn't matter what actual style the user chooses, our proxy style's methods (where implemented) will be used instead. 

For example, let's create a proxy style that forces all the screen text to be in uppercase, as follows:

 

class StyleOverrides(qtw.QProxyStyle):

 

def drawItemText(

self, painter, rect, flags, palette, enabled, text, textRole

):

"""Force uppercase in all text""" text = text.upper() super().drawItemText(

painter, rect, flags, palette, enabled, text, textRole

)

 

drawItemText() is the method called on the style whenever text must be drawn to the screen. It receives a number of arguments, but the one we're most concerned with is the text argument that is to be drawn. We're simply going to intercept this text and make it uppercase before passing all the arguments back to super().drawTextItem().

 

This proxy style can then be applied to our QApplication object in the same way as any other style:

 

if __name__ == '__main__':

app = qtw.QApplication(sys.argv)

proxy_style= StyleOverrides()

app.setStyle(proxy_style)

 

If you run the program at this point, you'll see that all the text is now uppercase. Mission accomplished!

 

----------------------

 

 

Drawing widgets

Now let's try something a bit more ambitious. Let's change all our QLineEdit entry boxes to a green rounded rectangle outline. So, how do we go about doing this in a proxy style?

The first step is to figure out what element of the widget we're trying to modify. These can be found as enum constants of the QStyle class, and they're divided into three main classes:

  • PrimitiveElement, which includes fundamental, non-interactive GUI elements such as frames or backgrounds
  • ControlElement, which includes interactive elements such as buttons or tabs
  • ComplexControl, which includes complex interactive elements such as combo boxes and sliders

Each of these classes of items is drawn by a different method of QStyle; in this case, it turns out that we want to modify the PE_FrameLineEdit element, which is a primitive element (as indicated by the PE_ prefix). This type of element is drawn by QStyle.drawPrimitive(), so we'll need to override that method in our proxy style.

Add this method to StyleOverrides, as follows:

def drawPrimitive(

self, element, option, painter, widget ):

"""Outline QLineEdits in Green"""

To control the drawing of an element, we need to issue commands to its painter object, as follows:

self.green_pen = qtg.QPen(qtg.QColor('green')) self.green_pen.setWidth(4)

if element == qtw.QStyle.PE_FrameLineEdit: painter.setPen(self.green_pen)

painter.drawRoundedRect(widget.rect(), 10, 10) else:

super().drawPrimitive(element, option, painter, widget)

Painter objects and the drawing will be fully covered in Chapter 12, Creating 2D Graphics with QPainter, but, for now, understand that the preceding code draws a green rounded rectangle if the element argument matches QStyle.PE_FrameLineEdit. Otherwise, it

passes the arguments to the superclass's drawPrimitive() method.

Notice that we do not call the superclass method after drawing our rectangle. If we did, then the superclass would draw its style-defined widget element on top of our green rectangle.

As you can see in this example, while working with QProxyStyle is considerably more esoteric than using palettes or style sheets, it does give us almost limitless control over how our widgets appear.

It doesn't matter whether you use QSS or styles and palettes to restyle an application; however, it is highly advised that you stick to one or the other. Otherwise, your style modifications can fight with one another and give unpredictable results across platforms and desktop settings.

Creating animations

Nothing quite adds a sophisticated edge to a GUI like the tasteful use of animations. Dynamic GUI elements that fade smoothly between changes in color, size, or position can add a modern touch to any interface.

Qt's animation framework allows us to create simple animations on our widgets using the QPropertyAnimation class. In this section, we'll explore how to use this class to spice up

our game lobby with some animations.

Because Qt style sheets override another widget- and palette-based styling, you will need to comment out all the style sheet code for these animations to work correctly.

Basic property animations

A QPropertyAnimation object is used to animate a single Qt property of a widget. The class automatically creates an interpolated series of steps between two numeric property values and applies the changes over time.

For example, let's animate our logo so that it scrolls out from left to right. You can begin by adding a property animation object, as follows:

self.heading_animation = qtc.QPropertyAnimation( heading, b'maximumSize')

QPropertyAnimation requires two arguments: a widget (or another type of QObject class) to be animated, and a bytes object indicating the property to be animated (note that this is a bytes object and not a string).

Next, we need to configure our animation object as follows:

self.heading_animation.setStartValue(qtc.QSize(10, logo.height())) self.heading_animation.setEndValue(qtc.QSize(400, logo.height())) self.heading_animation.setDuration(2000)

At the very least, we need to set a startValue value and an endValue value for the

property. Naturally, these values must be of the data type required by the property. We can also set duration in milliseconds (the default is 250).

Once configured, we just need to tell the animation to start, as follows:

self.heading_animation.start() There are a few requirements that limit what QPropertyAnimation objects can do:

  • The object to be animated must be a QObject subclass. This includes all widgets but excludes some Qt classes such as QPalette.
  • The property to be animated must be a Qt property (not just a Python member variable).
  • The property must have read-and-write accessor methods that require only a single value. For example, QWidget.size can be animated but not QWidget.width, because there is no setWidth() method.
  • The property value must be one of the following types: int, float, QLine, QLineF, QPoint, QPointF, QSize, QSizeF, QRect, QRectF, or QColor.

Unfortunately, for most widgets, these limitations exclude a number of aspects that we might want to animate—in particular, colors. Fortunately, we can work around this.

Animating colors

As you learned earlier in this chapter, widget colors are not properties of the widget – rather they are properties of the palette. The palette cannot be animated, because QPalette is not a subclass of QObject, and because setColor() requires more than just a single

value.

Colors are something that we'd like to animate, though; to make that happen, we need to subclass our widget and make its color settings into Qt properties.

Let's do that with a button; start a new class at the top of the script, as follows:

class ColorButton(qtw.QPushButton):

def _color(self):

return self.palette().color(qtg.QPalette.ButtonText)

def _setColor(self, qcolor):

palette = self.palette()

palette.setColor(qtg.QPalette.ButtonText, qcolor) self.setPalette(palette)

Here, we have a QPushButton subclass with accessor methods for the palette's ButtonText color. However, note that these are Python methods; in order to animate this property, we need color to be an actual Qt property. To correct this, we'll use the QtCore.pyqtProperty() function to wrap our accessor methods and create a property on the underlying Qt object.

You can do this as follows:

color = qtc.pyqtProperty(qtg.QColor, _color, _setColor)

The property name we use will be the name of the Qt property. The first argument passed is the data type required by the property, and the next two arguments are the getter and setter methods.

pyqtProperty() can also be used as a decorator, as follows:

@qtc.pyqtProperty(qtg.QColor)

def backgroundColor(self):

return self.palette().color(qtg.QPalette.Button)

@backgroundColor.setter

def backgroundColor(self, qcolor):

palette = self.palette()

palette.setColor(qtg.QPalette.Button, qcolor) self.setPalette(palette)

Notice that, in this approach, both methods must be named identically using the property name we intend to create.

Now that our properties are in place, we need to replace our regular QPushButton objects with ColorButton objects:

  • Replace these definitions
  • at the top of the MainWindow constructor

self.submit = ColorButton( 'Connect',

clicked=lambda: qtw.QMessageBox.information( None,

'Connecting',

'Prepare for Battle!'))

self.cancel = ColorButton(

'Cancel',

clicked=self.close)

With these changes made, we can animate the color values, as follows:

self.text_color_animation = qtc.QPropertyAnimation(

self.submit, b'color') self.text_color_animation.setStartValue(qtg.QColor('#FFF')) self.text_color_animation.setEndValue(qtg.QColor('#888')) self.text_color_animation.setLoopCount(-1) self.text_color_animation.setEasingCurve( qtc.QEasingCurve.InOutQuad) self.text_color_animation.setDuration(2000) self.text_color_animation.start()

This works like a charm. We've also added a couple of additional configuration settings here:

  • setLoopCount() will set how many times the animation restarts. A value of -1 will make it loop forever.
  • setEasingCurve() changes the curve along which the values are interpolated. We've chosen InOutQuad, which slows the rate of the start and finish of the animation.

Now, when you run the script, note that the color fades from white to gray and then immediately loops back to white. If we want an animation to move from one value to another and then smoothly back again, we can use the setKeyValue() method to put a value in the middle of the animation:

self.bg_color_animation = qtc.QPropertyAnimation(

self.submit, b'backgroundColor') self.bg_color_animation.setStartValue(qtg.QColor('#000'))

self.bg_color_animation.setKeyValueAt(0.5, qtg.QColor('darkred')) self.bg_color_animation.setEndValue(qtg.QColor('#000'))

self.bg_color_animation.setLoopCount(-1) self.bg_color_animation.setDuration(1500)

In this case, our start and end values are the same, and we've added a value at 0.5 (50% of the way through the animation) set to a second color. This animation will fade from black to dark red and back again. You can add as many key values as you wish and make quite complex animations.

Using animation groups

As we add more and more animations to a GUI, we may find it necessary to group them together so that we can control the animations as a group. This can be done using the animation group classes, QParallelAnimationGroup and

QSequentialAnimationGroup.

Both of these classes allow us to add multiple animations to the group and start, stop, pause, and resume the animations as a group.

For example, let's group our button animations as follows:

self.button_animations = qtc.QParallelAnimationGroup() self.button_animations.addAnimation(self.text_color_animation) self.button_animations.addAnimation(self.bg_color_animation)

QParallelAnimationGroup plays all animations in parallel whenever its start() method is called. In contrast, QSequentialAnimationGroup will playback its animations one at a time in the order added, as shown in the following code block:

self.all_animations = qtc.QSequentialAnimationGroup() self.all_animations.addAnimation(self.heading_animation) self.all_animations.addAnimation(self.button_animations) self.all_animations.start()

By adding animation groups to other animation groups as we've done here, we can choreograph complex arrangements of animations into one object that can be started, stopped, or paused altogether.

Comment out all the other animation start() calls and launch the script. Note that the button animations start only after the heading animation has finished.

We will explore more uses of QPropertyAnimation in Chapter 12, 2D Graphics with QPainter.

Summary

In this chapter, we learned how to customize the look and feel of a PyQt application. We also learned how to manipulate screen fonts and add images. Additionally, we learned how to package image and font resources in a way that is resilient to path changes. We also explored how to alter the color and appearance of the application using palettes and style sheets, and how to override style methods to implement nearly limitless style changes. Finally, we explored widget animation using Qt's animation framework and learned how to add custom Qt properties to our classes so that we can animate them.

In the next chapter, we're going to explore the world of multimedia applications using the QtMultimedia library. You'll learn how to work with cameras to take pictures and videos, how to display video content, and how to record and playback audio.

Questions

Try these questions to test your knowledge from this chapter:

  1. You are preparing to distribute your text editor application, and want to ensure that the user is given a monospaced font by default, no matter what platform they use. What two ways can you use to accomplish this?
  2. As closely as possible, try to mimic the following text using QFont:

  1. Can you explain the difference between QImage, QPixmap, and QIcon?
  2. You have defined the following .qrc file for your application, run pyrcc5, and imported the resource library in your script. How would you load this image into QPixmap?

pc_img.45234.png

  1. Using QPalette, how would you tile the background of a QWidget object with the tile.png image?
  2. You are trying to make a delete button pink using QSS, but it's not working. What is wrong with your code?

deleteButton = qtw.QPushButton('Delete') form.layout().addWidget(deleteButton)

form.setStyleSheet(

form.styleSheet() + 'deleteButton{ background-color: #8F8; }' )

  1. Which style sheet string will turn the background colors of your QLineEdit widget black?

stylesheet1 = "QWidget {background-color: black;}" stylesheet2 = ".QWidget {background-color: black;}"

  1. Build a simple app with a combo box that allows you to change the Qt style to any style installed on your system. Include some other widgets so that you can see how they look in the different styles.
  2. You feel very happy about learning how to style PyQt apps and want to create a QProxyStyle class that will force all pixmaps in a GUI to be smile.gif. How would you do this? Hint: You will need to research some other drawing methods of QStyle than the ones discussed in this chapter.
  3. The following animation doesn't work; figure out why it doesn't work:

class MyWidget(qtw.QWidget):

def __init__(self): super().__init__()

animation = qtc.QPropertyAnimation( self, b'windowOpacity') animation.setStartValue(0) animation.setEndValue(1) animation.setDuration(10000) animation.start()

Further reading

For further information, please refer to the following:

[ 156 ] )

'Mastering-GUI-Programming' 카테고리의 다른 글

Building Applications with QMainWindow  (0) 2023.03.08
Handling Events with Signals and Slots  (0) 2023.03.08
Building Forms with QtWidgets  (0) 2023.03.08
Deep Dive into PyQt  (0) 2023.03.08

댓글