Форум программистов, компьютерный форум, киберфорум
iamvic
Войти
Регистрация
Восстановить пароль
Рейтинг: 5.00. Голосов: 1.

К вопросу об "усыновлении сирот" в PyQt5.

Запись от iamvic размещена 02.07.2021 в 18:35
Метки pyqt5, python, python 3, qt5

После прочтения весьма дельных советов от Fudthhh в Просьба о рецензировании кода формы PyQt5
приходится писать очередную памятку себе, поскольку есть грех - частенько забываю явно указывать
родителей у объектов при построении графического интерфейса.

В документации-то, конечно, все нижеприведённые факты помянуты и, помнится, даже дискуссии
какие-то бурные читал в Интернете на эту тему, но без наглядной демонстрации происходящего понять
было тяжело, поэтому для примера был взят этот образец:
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtCore import QObject
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QDialog
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QPushButton
 
class ProbeBtn(QPushButton):
    def __init__(self, text='', parent=None):
        super(ProbeBtn, self).__init__(parent)
        self.setObjectName(text)
        self.setText(text)
 
    def keyPressEvent(self, e):
        print(self.objectName(),
              'keyPressEvent() self.sender() =',
              self.sender())
        super(ProbeBtn, self).keyPressEvent(e)
 
    def mousePressEvent(self, e):
        print(self.objectName(),
              'mousePressEvent() self.sender() =',
              self.sender())
        super(ProbeBtn, self).mousePressEvent(e)
 
class ProbeDlg(QDialog):
    def __init__(self, parent=None):
        super(ProbeDlg, self).__init__(parent)
        self.setObjectName(self.metaObject().className())
        self.initUI()
 
    def initUI(self):
        self.obj = QObject()
        self.vbox = QVBoxLayout()
        self.orphan = ProbeBtn('Orphan_Button')
        self.infant = ProbeBtn('Infant_Button', self)
 
        self.infant.pressed.connect(self.on_infant_pressed)
        self.orphan.pressed.connect(self.on_orphan_pressed)
 
        self.vbox.addWidget(self.infant)
        self.vbox.addWidget(self.orphan)
        self.vbox.addStretch(1)
        self.setLayout(self.vbox)
 
    def on_infant_pressed(self):
        print(self.objectName(),
              'on_infant_pressed() self.sender().objectName() =',
              self.sender().objectName())
        print()
        pass
 
    def on_orphan_pressed(self):
        print(self.objectName(),
              'on_orphan_pressed() self.sender().objectName() =',
              self.sender().objectName())
        print()
        pass
 
if __name__ == '__main__':
    print()
    app = QApplication(sys.argv)
 
    mwin = ProbeDlg()
    mwin.show()
    sys.exit(app.exec_())
В этом примере из четырех объектов, создаваемых при инициализации графического интерфейса,
родитель явно указан только у одного. Остальные родителей не имеют. Тем не менее, внешне
всё выглядит вполне благопристойно - диалог открывается, кнопки нажимаются, события происходят,
реакция на них ожидаемая и прочее.

Если добавить ещё немножко печати, то можно кое-что понять из того, что там происходит:
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtCore import QObject
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QDialog
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QPushButton
 
class ProbeBtn(QPushButton):
    def __init__(self, text='', parent=None):
        super(ProbeBtn, self).__init__(parent)
        self.setObjectName(text)
        self.setText(text)
        print(self.objectName(),
              '__init__() parent =',
              parent)
        print(self.objectName(),
              '__init__() self =',
              self)
        print(self.objectName(),
              '__init__() self.parent() =',
              self.parent())
        print()
 
    def keyPressEvent(self, e):
        print(self.objectName(),
              'keyPressEvent() self.sender() =',
              self.sender())
        super(ProbeBtn, self).keyPressEvent(e)
 
    def mousePressEvent(self, e):
        print(self.objectName(),
              'mousePressEvent() self.sender() =',
              self.sender())
        super(ProbeBtn, self).mousePressEvent(e)
 
class ProbeDlg(QDialog):
    def __init__(self, parent=None):
        super(ProbeDlg, self).__init__(parent)
        self.setObjectName(self.metaObject().className())
        self.initUI()
 
    def initUI(self):
        print('---> start init')
        self.obj = QObject()
        self.vbox = QVBoxLayout()
        self.orphan = ProbeBtn('Orphan_Button')
        self.infant = ProbeBtn('Infant_Button', self)
 
        self.infant.pressed.connect(self.on_infant_pressed)
        self.orphan.pressed.connect(self.on_orphan_pressed)
 
        print('---> after init')
        print(self.objectName(),
              'initUI() self.obj.parent() =',
              self.obj.parent())
        print(self.objectName(),
              'initUI() self.vbox.parent() =',
              self.vbox.parent())
        print(self.objectName(),
              'initUI() self.orphan.parent() =',
              self.orphan.parent())
        print(self.objectName(),
              'initUI() self.infant.parent() =',
              self.infant.parent())
        print()
 
        print(self.objectName(),
              'initUI() self.children() =',
              self.children())
        print()
 
        self.vbox.addWidget(self.infant)
        self.vbox.addWidget(self.orphan)
        self.vbox.addStretch(1)
        self.setLayout(self.vbox)
 
        print('---> after adding to dialog')
        print(self.objectName(),
              'initUI() self.obj.parent() =',
              self.obj.parent())
        print(self.objectName(),
              'initUI() self.vbox.parent() =',
              self.vbox.parent())
        print(self.objectName(),
              'initUI() self.orphan.parent() =',
              self.orphan.parent())
        print(self.objectName(),
              'initUI() self.infant.parent() =',
              self.infant.parent())
        print()
 
        print(self.objectName(),
              'initUI() self.children() =',
              self.children())
        print()
 
    def on_infant_pressed(self):
        print(self.objectName(),
              'on_infant_pressed() self.sender().objectName() =',
              self.sender().objectName())
        print()
        pass
 
    def on_orphan_pressed(self):
        print(self.objectName(),
              'on_orphan_pressed() self.sender().objectName() =',
              self.sender().objectName())
        print()
        pass
 
if __name__ == '__main__':
    print()
    app = QApplication(sys.argv)
 
    mwin = ProbeDlg()
    mwin.show()
    sys.exit(app.exec_())
Разбирая собранный протокол работы, можно сделать следующие выводы:

1. инициализациия
Код:
user@linux:~> python3 orin_probe.py

---> start init
Orphan_Button __init__() parent = None
Orphan_Button __init__() self = <__main__.ProbeBtn object at 0x7f4abb309f78>
Orphan_Button __init__() self.parent() = None

Infant_Button __init__() parent = <__main__.ProbeDlg object at 0x7f4abb309dc8>
Infant_Button __init__() self = <__main__.ProbeBtn object at 0x7f4aac101048>
Infant_Button __init__() self.parent() = <__main__.ProbeDlg object at 0x7f4abb309dc8>
Как и ожидалось, кнопка Orphan_Button создаётся без родителя, в отличие от Infant_Button.

2. состояние отношений объектов после инициализациии
Код:
---> after init
ProbeDlg initUI() self.obj.parent() = None
ProbeDlg initUI() self.vbox.parent() = None
ProbeDlg initUI() self.orphan.parent() = None
ProbeDlg initUI() self.infant.parent() = <__main__.ProbeDlg object at 0x7f4abb309dc8>

ProbeDlg initUI() self.children() = [<__main__.ProbeBtn object at 0x7f4aac101048>]
Все объекты, у которых не было родителя, ожидаемо не попали в список детей главного окна приложения,
в качестве которого используется производный от QDialog класс.

3. состояние отношений объектов после добавления в диалог
Код:
---> after adding to dialog
ProbeDlg initUI() self.obj.parent() = None
ProbeDlg initUI() self.vbox.parent() = <__main__.ProbeDlg object at 0x7f4abb309dc8>
ProbeDlg initUI() self.orphan.parent() = <__main__.ProbeDlg object at 0x7f4abb309dc8>
ProbeDlg initUI() self.infant.parent() = <__main__.ProbeDlg object at 0x7f4abb309dc8>

ProbeDlg initUI() self.children() = [<__main__.ProbeBtn object at 0x7f4aac101048>,
<PyQt5.QtWidgets.QVBoxLayout object at 0x7f4abb309ee8>,
<__main__.ProbeBtn object at 0x7f4abb309f78>]
Однако, после создания графического интерфейса в окне диалога, как говорится,
"детей у папаши прибавилось" и у всех виджетов-"сирот", участвоваших в создании
диалога, появился родитель. А вот self.obj так и остался "сиротой".

4. реакции на управляющие воздействия
Код:
Infant_Button mousePressEvent() self.sender() = None
ProbeDlg on_infant_pressed() self.sender().objectName() = Infant_Button

Orphan_Button mousePressEvent() self.sender() = None
ProbeDlg on_orphan_pressed() self.sender().objectName() = Orphan_Button

Orphan_Button keyPressEvent() self.sender() = None
Infant_Button keyPressEvent() self.sender() = None
ProbeDlg on_infant_pressed() self.sender().objectName() = Infant_Button

Infant_Button keyPressEvent() self.sender() = None
Orphan_Button keyPressEvent() self.sender() = None
ProbeDlg on_orphan_pressed() self.sender().objectName() = Orphan_Button

user@linux:~>
Реакция на нажатие клавиш клавиатуры и мыши ожиданий не нарушает.

Хорошо это или плохо - такое неявное "усыновление сирот" при создании графического интерфейса -
не очень понятно. С одной стороны, это позволяет более терпимо относиться к "забывчивости"
программиста и создавать работающий графический интерфейс даже в условиях неполной информации.
С другой стороны, всё что не обязательно постепенно становится ненужным. И тут уже возможны
всяческие неприятности, потому что объекты без родителя, подобные self.obj из примера, требуют
к себе особого отношения, а вылавливать их весьма затруднительно, если отсутствие родителя
не зафиксировано явно.

Один из выходов - приучить себя всегда явно указывать отсутствие или наличие родителя
при создании любых объектов. Насколько это достижимо - каждый решает сам
Размещено в Памятка
Показов 663 Комментарии 5
Всего комментариев 5
Комментарии
  1. Старый комментарий
    Аватар для Fudthhh
    Python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    import sys
    from PyQt5 import QtWidgets, QtCore
     
     
    class Example(QtWidgets.QWidget):
        def __init__(self, parent: QtWidgets.QWidget = None):
            QtWidgets.QWidget.__init__(self, parent)
     
            lineEdit1 = QtWidgets.QLineEdit(self)
            lineEdit1.setObjectName("lineEdit1")
            lineEdit1.setGeometry(10, 10, 200, 30)
     
            self.lineEdit2 = QtWidgets.QLineEdit()
            self.lineEdit2.setObjectName("lineEdit2")
            self.lineEdit2.setGeometry(50, 200, 200, 30)
     
            if not parent:
                self.show()
     
     
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        main = Example()
        print(main.findChild(QtWidgets.QLineEdit, "lineEdit1"))
        # Найдет его даже в рекурсивно миллионной дочерней форме если
        # у всех форм есть родитель который привидет к главному окну
        # << <PyQt5.QtWidgets.QLineEdit object at 0x08630580>
        print(main.findChild(QtWidgets.QLineEdit, "lineEdit2"))
        # Очевидно, виджет не найден, а так как у него нет родителя
        # собиратель мусора просто сносит этот обьект
        # None
        sys.exit(app.exec_())
     
        # P.S. если использовать layout'ы то можно наблюдать что родитель
        # у виджета все же появляется.
    Запись от Fudthhh размещена 09.07.2021 в 14:51 Fudthhh вне форума
  2. Старый комментарий
    Со всем уважением, Fudthhh, но боюсь, что Ваше утверждение
    Python
    1
    2
    3
    
        # Очевидно, виджет не найден, а так как у него нет родителя
        # собиратель мусора просто сносит этот обьект
        # None

    Не по теме:


    не совсем верно :(. Насколько я понимаю, QWidget наследуется от QObject, а экземпляры
    QObject, не имеющие родителя, не уничтожаются сборщиком мусора до тех пор, пока они
    не будут помечены к удалению с помощью QObject.deleteLater(). Например, это прямо следует
    из необходимости подключения сигнала QThread.finished() к слоту QObject.deleteLater()
    для освобождения объектов в потоке. Я могу ошибаться, но GUI-поток, наверное, не является
    исключением из общего правила? Да, такие объекты будут в конце концов уничтожены, но только
    после завершения работы приложения. Почему я и говорю о необходимости особого отношения
    к объектам без родителей, чтобы не нарываться на утечки.



    А вот за необходимость именования всех объектов я голосую обеими руками! Вкупе с findChild()
    это снимает массу головной боли в процессе сопровождения задачи.

    Продолжение следует...
    Запись от iamvic размещена 10.07.2021 в 11:18 iamvic вне форума
    Обновил(-а) iamvic 14.07.2021 в 14:58 (вычеркнул свой бред)
  3. Старый комментарий
    На отдельном примере попытаюсь показать то, что сбивает с толку при построении
    графического интерфейса, расхолаживает и позволяет наплевательски относиться
    к требованию явного указания родителей:
    Python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    
    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    import sys
    from PyQt5.QtCore import QObject
    from PyQt5.QtWidgets import QApplication
    from PyQt5.QtWidgets import QHBoxLayout
    from PyQt5.QtWidgets import QLabel
    from PyQt5.QtWidgets import QLineEdit
    from PyQt5.QtWidgets import QPushButton
    from PyQt5.QtWidgets import QVBoxLayout
    from PyQt5.QtWidgets import QWidget
     
     
     
    def print_parent(client):
        print()
        print('initUI() self =',
              client)
        print('initUI() self.parent() =',
              client.parent())
        print('initUI() self.namelbl.parent() =',
              client.namelbl.parent())
        print('initUI() self.nametxt.parent() =',
              client.nametxt.parent())
        print('initUI() self.savebtn.parent() =',
              client.savebtn.parent())
        print('initUI() self.dropbtn.parent() =',
              client.dropbtn.parent())
        print('initUI() self.namebox.parent() =',
              client.namebox.parent())
        print('initUI() self.pushbox.parent() =',
              client.pushbox.parent())
        print('initUI() self.mainbox.parent() =',
              client.mainbox.parent())
        print('initUI() self.mainbox =',
              client.mainbox)
        print()
     
        print('initUI() self.children() =',
              client.children())
        print()
     
     
     
    class ProbeWidget(QWidget):
        def __init__(self, parent=None):
            super(ProbeWidget, self).__init__(parent)
            self.setObjectName(self.metaObject().className())
            self.initUI()
     
        def initUI(self):
            # В принципе, абсолютно безразлично есть или нет родители у элементов,
            # помещаемых в форму с помощью раскладок (layouts).
            # Всё равно, в конце концов эти элементы будут отняты у своих родителей
            # (кто бы они ни были) и отданы на попечение другому.
     
            self.namelbl = QLabel('Наименование', self)
            self.namelbl.setObjectName('namelbl')
            # родитель self.namelbl - создаваемый экземпляр класса ProbeWidget
     
            self.nametxt = QLineEdit(self.namelbl)
            self.nametxt.setObjectName('nametxt')
            # родитель self.nametxt - метка self.namelbl (вот такой каприз :))
     
            self.savebtn = QPushButton('Сохранить', self.nametxt)
            self.savebtn.setObjectName('savebtn')
            # родитель self.savebtn - поле ввода self.nametxt (а чё такого? :))
     
            self.dropbtn = QPushButton('Отказаться', None)
            self.dropbtn.setObjectName('dropbtn')
            # self.dropbtn - "сирота"
     
            self.namebox = QVBoxLayout(None)
            self.namebox.setObjectName('namebox')
            # self.namebox должен быть "сиротой" по определению
            # (иначе нарвёмся на чудеса раскладки)
     
            self.pushbox = QHBoxLayout(None)
            self.pushbox.setObjectName('pushbox')
            # self.pushbox должен быть "сиротой" по определению
            # (иначе нарвёмся на чудеса раскладки)
     
            self.mainbox = QVBoxLayout(None)
            self.mainbox.setObjectName('mainbox')
            # self.mainbox должен быть "сиротой" по определению
            # (иначе забракует self.setLayout())
     
            self.namebox.addWidget(self.namelbl)
            self.namebox.addWidget(self.nametxt)
            # раскладка элементов ввода
     
            self.pushbox.addWidget(self.savebtn)
            self.pushbox.addStretch(1)
            self.pushbox.addWidget(self.dropbtn)
            # раскладка кнопок
     
            print('---> before adding to main layout')
            print_parent(self)
     
            self.mainbox.addLayout(self.namebox)
            self.mainbox.addStretch(1)
            self.mainbox.addLayout(self.pushbox)
            # общая раскладка
     
            print('---> before self.setLayout()')
            print_parent(self)
     
            self.setLayout(self.mainbox)
     
            print('---> after self.setLayout()')
            print_parent(self)
     
     
     
    if __name__ == '__main__':
        app = QApplication(sys.argv)
     
        mwin = ProbeWidget()
        mwin.show()
        sys.exit(app.exec_())
    Продолжение следует...
    Запись от iamvic размещена 10.07.2021 в 11:19 iamvic вне форума
  4. Старый комментарий
    Из собранного протокола работы:
    Код:
    user@linux:~> python3 orin_probe.py
    ---> before adding to main layout
    
    initUI() self = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.parent() = None
    initUI() self.namelbl.parent() = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.nametxt.parent() = <PyQt5.QtWidgets.QLabel object at 0x7fbdc69bfb88>
    initUI() self.savebtn.parent() = <PyQt5.QtWidgets.QLineEdit object at 0x7fbdc69bfc18>
    initUI() self.dropbtn.parent() = None
    initUI() self.namebox.parent() = None
    initUI() self.pushbox.parent() = None
    initUI() self.mainbox.parent() = None
    initUI() self.mainbox = <PyQt5.QtWidgets.QVBoxLayout object at 0x7fbdc69bfee8>
    
    initUI() self.children() = [<PyQt5.QtWidgets.QLabel object at 0x7fbdc69bfb88>]
    
    ---> before self.setLayout()
    
    initUI() self = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.parent() = None
    initUI() self.namelbl.parent() = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.nametxt.parent() = <PyQt5.QtWidgets.QLabel object at 0x7fbdc69bfb88>
    initUI() self.savebtn.parent() = <PyQt5.QtWidgets.QLineEdit object at 0x7fbdc69bfc18>
    initUI() self.dropbtn.parent() = None
    initUI() self.namebox.parent() = <PyQt5.QtWidgets.QVBoxLayout object at 0x7fbdc69bfee8>
    initUI() self.pushbox.parent() = <PyQt5.QtWidgets.QVBoxLayout object at 0x7fbdc69bfee8>
    initUI() self.mainbox.parent() = None
    initUI() self.mainbox = <PyQt5.QtWidgets.QVBoxLayout object at 0x7fbdc69bfee8>
    
    initUI() self.children() = [<PyQt5.QtWidgets.QLabel object at 0x7fbdc69bfb88>]
    
    ---> after self.setLayout()
    
    initUI() self = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.parent() = None
    initUI() self.namelbl.parent() = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.nametxt.parent() = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.savebtn.parent() = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.dropbtn.parent() = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.namebox.parent() = <PyQt5.QtWidgets.QVBoxLayout object at 0x7fbdc69bfee8>
    initUI() self.pushbox.parent() = <PyQt5.QtWidgets.QVBoxLayout object at 0x7fbdc69bfee8>
    initUI() self.mainbox.parent() = <__main__.ProbeWidget object at 0x7fbdc69bfaf8>
    initUI() self.mainbox = <PyQt5.QtWidgets.QVBoxLayout object at 0x7fbdc69bfee8>
    
    initUI() self.children() = [<PyQt5.QtWidgets.QLabel object at 0x7fbdc69bfb88>, <PyQt5.QtWidgets.QVBoxLayout object at 0x7fbdc69bfee8>,
    <PyQt5.QtWidgets.QLineEdit object at 0x7fbdc69bfc18>,
    <PyQt5.QtWidgets.QPushButton object at 0x7fbdc69bfca8>,
    <PyQt5.QtWidgets.QPushButton object at 0x7fbdc69bfd38>]
    
    user@linux:~>
    следует, что раскладки получают родителя в момент включения в другую раскладку,
    а всем остальным соучастникам родителя назначает окончательный setLayout(),
    независимо от того, был у них родитель или нет.

    И человек, как существо мыслящее , начинает задумываться "А на хрена тут уродоваться
    с явным указанием родителя, если раскладкам он не нужен по определению, а все остальные
    будут отняты у своих родителей и переданы другому во время исполнения окончательного
    setLayout()?"
    И легко принимает решение вообще не указывать родителя в этом случае,
    что уже само по себе чревато утечками. Как тут быть - я не знаю.
    Запись от iamvic размещена 10.07.2021 в 11:21 iamvic вне форума
  5. Старый комментарий
    Едрить твою налево! Ржу не могу!!! И ведь было же сомнение, что про виджеты я чушь несу в комменте,
    который начинается со слов "Со всем уважением,...", что там всё не так, как у простых QObject.
    Простите все, кто может. А коммент тот оставлю "дабы дурость моя всем видна была"
    но бред вычеркнул

    Короче, виджеты вообще не удаляются, если у них не установлен атрибут Qt.WA_DeleteOnClose.
    А если он есть, то виджет удаляется при закрытии независимо от наличия родителя.
    Запись от iamvic размещена 11.07.2021 в 20:20 iamvic вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2021, vBulletin Solutions, Inc.