├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
└── src
├── Files
├── johnwacenwa.png
├── logo.png
├── randomize.png
├── redo.png
├── tess.gif
└── undo.png
├── ImageWindow.py
├── MainWindow.py
├── MultiWindowTest.py
└── requirements.txt
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105 | __pypackages__/
106 |
107 | # Celery stuff
108 | celerybeat-schedule
109 | celerybeat.pid
110 |
111 | # SageMath parsed files
112 | *.sage.py
113 |
114 | # Environments
115 | .env
116 | .venv
117 | env/
118 | venv/
119 | ENV/
120 | env.bak/
121 | venv.bak/
122 |
123 | # Spyder project settings
124 | .spyderproject
125 | .spyproject
126 |
127 | # Rope project settings
128 | .ropeproject
129 |
130 | # mkdocs documentation
131 | /site
132 |
133 | # mypy
134 | .mypy_cache/
135 | .dmypy.json
136 | dmypy.json
137 |
138 | # Pyre type checker
139 | .pyre/
140 |
141 | # pytype static type analyzer
142 | .pytype/
143 |
144 | # Cython debug symbols
145 | cython_debug/
146 |
147 | # PyCharm
148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
150 | # and can be added to the global gitignore or merged into this file. For a more nuclear
151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
152 | #.idea/
153 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Aniket Rajnish
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MultiWindowSync-PyQt
2 | * Windows GUI application developed using `PyQt5`.
3 | * It demonstrates the synchronization of multiple windows using `pyqtSignal`.
4 | * Inspired by the [work](https://twitter.com/_nonfigurativ_/status/1727322594570027343) of Bjørn Staal.
5 |
6 | 
7 |
8 | https://github.com/aniketrajnish/MultiWindowSync-PyQt/assets/58925008/e00e10e9-6373-46d8-9669-77466ee8bd90
9 |
10 | ## Usage
11 | * Clone the repository
12 | ```
13 | git clone https://github.com/aniketrajnish/MultiWindowSync-PyQt.git
14 | ```
15 | * Open Terminal and change directory to the script's folder.
16 | ```
17 | cd \src
18 | ```
19 | * Install Dependencies
20 | ```
21 | pip install -r requirements.txt
22 | ```
23 | * Run the main script
24 | ```
25 | python MultiWindowTest.py
26 | ```
27 | * Use your own image/GIF.
28 | ```
29 | File -> Open Image/GIF
30 | ```
31 | * In case you don't wanna go through all of this hassle, I've added an executable file in the [Releases Section](https://github.com/aniketrajnish/MultiWindowSync-PyQt/releases/tag/v001) that you can directly try on your machine.
32 |
33 | ## Contributing
34 | Contributions to the project are welcome. Currently working on:
35 | * Expanding the environment to 3D using PyOpenGL.
36 | * Fix the bug where the parent image window always moves the image along with it for ref to other windows even if `Move With Window` is unchecked.
37 |
38 | ## License
39 | MIT License
40 |
--------------------------------------------------------------------------------
/src/Files/johnwacenwa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aniketrajnish/MultiWindowSync-PyQt/e844ca90f6c0f330889c2acb512b5cab259be145/src/Files/johnwacenwa.png
--------------------------------------------------------------------------------
/src/Files/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aniketrajnish/MultiWindowSync-PyQt/e844ca90f6c0f330889c2acb512b5cab259be145/src/Files/logo.png
--------------------------------------------------------------------------------
/src/Files/randomize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aniketrajnish/MultiWindowSync-PyQt/e844ca90f6c0f330889c2acb512b5cab259be145/src/Files/randomize.png
--------------------------------------------------------------------------------
/src/Files/redo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aniketrajnish/MultiWindowSync-PyQt/e844ca90f6c0f330889c2acb512b5cab259be145/src/Files/redo.png
--------------------------------------------------------------------------------
/src/Files/tess.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aniketrajnish/MultiWindowSync-PyQt/e844ca90f6c0f330889c2acb512b5cab259be145/src/Files/tess.gif
--------------------------------------------------------------------------------
/src/Files/undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aniketrajnish/MultiWindowSync-PyQt/e844ca90f6c0f330889c2acb512b5cab259be145/src/Files/undo.png
--------------------------------------------------------------------------------
/src/ImageWindow.py:
--------------------------------------------------------------------------------
1 | import random
2 | from PyQt5.QtWidgets import *
3 | from PyQt5.QtGui import *
4 | from PyQt5.QtGui import *
5 | from PyQt5.QtCore import *
6 |
7 | class ImageWindow(QMainWindow):
8 | '''
9 | Each window instance is used to display images and GIFs.
10 | '''
11 | imageMoved = pyqtSignal(QPoint) # Signals b/w windows when an image is moved
12 | windowClosing = pyqtSignal(object) # Signals b/w windows when any window closes
13 |
14 | def __init__(self, parent=None, imagePath=None):
15 | '''
16 | The settings and window is intialized as well the path of the image to be shown is assigned.
17 | '''
18 | super().__init__(parent)
19 | self.initUI()
20 | self.initSettings(imagePath)
21 |
22 | def setMoveWithWindow(self, state):
23 | self.moveWithWindow = state
24 |
25 | def setKeepCentered(self, state):
26 | self.keepCentered = state
27 |
28 | def initUI(self):
29 | '''
30 | The window is initialized randomly in a bounding box that's relative to the screen.
31 | The Image Label to hold the image is given the same size as the window.
32 | '''
33 | screen = QApplication.primaryScreen().size()
34 | screenWidth = screen.width()
35 | screenHeight = screen.height()
36 |
37 | self.setGeometry(random.randint(screenWidth//4, screenWidth//2),
38 | random.randint(0, screenHeight//2),
39 | int(screenWidth//2.5), int(screenHeight//2))
40 |
41 | self.setWindowIcon(QIcon('Files/logo.png'))
42 | self.setWindowTitle('Image Window')
43 | self.imageLabel = QLabel(self)
44 | self.imageLabel.setGeometry(self.rect())
45 |
46 | def initSettings(self, imagePath):
47 | '''
48 | The image path is assigned to the window as well as the update function to update the image position is initialized.
49 | The settings from the main window are initialized as well.
50 | '''
51 | self.currentImagePath = imagePath
52 | self.movie = None
53 |
54 | self.moveWithWindow = False
55 | self.keepCentered = False
56 |
57 | if imagePath:
58 | self.loadImage(imagePath, 1)
59 | self.updateFunction(1)
60 |
61 | def updateFunction(self, timeStep):
62 | '''
63 | A timer to keep track and update the image's position until the window is moved.
64 | timeStep determines how often the update function is called and can be controlled by radio buttons in the main window.
65 | '''
66 | self.timer = QTimer(self)
67 | self.timer.timeout.connect(self.updateImagePosition)
68 | self.timer.start(timeStep)
69 | self.isImageMoved = False # Flag to check if the image window has been moved
70 |
71 | def moveEvent(self, event):
72 | '''
73 | Override the default move event to communicate the positon between different windows when an Image window is moved.
74 | It ensures that image remains at a consistent position relative to the other windows.
75 | '''
76 | if not self.moveWithWindow: # Return if 'Move With Window' has not been checked in the main window
77 | return
78 | super().moveEvent(event)
79 | if self.currentImagePath:
80 | globalPos = self.mapToGlobal(self.imageLabel.pos())
81 | self.imageMoved.emit(globalPos) # Communicate the image position to different windows
82 | self.isImageMoved = True # Indicated that window has been moved manually
83 |
84 | def loadImage(self, imagePath, scaleFactor):
85 | '''
86 | Load the Image/GIF into the window and apply the scaleFactor.
87 | Decides between QMovie and QPixmap based on the type of image.
88 | If .gif it uses QMovie, else QPixmap.
89 | '''
90 | self.currentImagePath = imagePath
91 | if imagePath.lower().endswith('.gif'):
92 | self.movie = QMovie(imagePath)
93 | self.movie.setScaledSize(self.imageLabel.size() * scaleFactor)
94 | self.imageLabel.setMovie(self.movie)
95 | self.movie.start()
96 | self.initScale = self.imageLabel.size() # Storing intial scale
97 | else:
98 | self.movie = None
99 | pixmap = QPixmap(imagePath)
100 | if not pixmap.isNull():
101 | scaledPixmap = pixmap.scaled(self.size() * scaleFactor, Qt.KeepAspectRatio, Qt.SmoothTransformation)
102 | self.imageLabel.setPixmap(scaledPixmap)
103 | self.initScale = self.size() # Storing initial scale
104 |
105 | def centerImage(self):
106 | '''
107 | Move the image/GIF to the center of the window.
108 | '''
109 | if self.movie:
110 | rect = QRect(0, 0, self.movie.scaledSize().width(), self.movie.scaledSize().height())
111 | elif self.imageLabel.pixmap() and not self.imageLabel.pixmap().isNull():
112 | pixmap = self.imageLabel.pixmap()
113 | rect = pixmap.rect()
114 | else:
115 | return
116 |
117 | rect.moveCenter(self.rect().center())
118 | self.imageLabel.setGeometry(rect)
119 |
120 | def updateImagePosition(self):
121 | '''
122 | Used to update the position of image ensuring that the image remains at a consistent position relative to the other windows.
123 | If the user has "Keep Centered" checked it aligns the images of the active window to the center and aligns images of other windows around it.
124 | '''
125 | if self.isImageMoved: # Skip if window was moved, then moveEvent governs the image position
126 | return
127 |
128 | if self.currentImagePath:
129 | activeWindow = QApplication.activeWindow()
130 |
131 | if self == activeWindow and self.keepCentered:
132 | self.centerImage()
133 | return
134 |
135 | if self.parent().imageWindows:
136 | if isinstance(activeWindow, ImageWindow) and self.keepCentered:
137 | refWindow = activeWindow # Active window is the one that's selected and it governs the image positions
138 | else:
139 | refWindow = self.parent().imageWindows[0] # If no window has been selected the first one governs the image positions
140 | globalPos = refWindow.mapToGlobal(refWindow.imageLabel.pos())
141 | localPos = self.mapFromGlobal(globalPos)
142 | self.imageLabel.move(localPos)
143 | self.update()
144 |
145 |
146 | def restartGif(self):
147 | '''Used to sync the GIF animation across all windows.'''
148 | if self.currentImagePath and self.currentImagePath.lower().endswith('.gif'):
149 | if self.movie:
150 | self.movie.stop()
151 | self.movie.start()
152 |
153 | def setScale(self, scaleFactor):
154 | '''
155 | Scale the image and its label based on the user input by using the scale slider.
156 | '''
157 | if self.movie and self.initScale:
158 | newSize = self.initScale * scaleFactor
159 | self.movie.setScaledSize(newSize)
160 | elif self.imageLabel.pixmap() and not self.imageLabel.pixmap().isNull() and self.initScale:
161 | newSize = self.initScale * scaleFactor
162 | scaledPixmap = QPixmap(self.currentImagePath).scaled(newSize, Qt.KeepAspectRatio, Qt.SmoothTransformation)
163 | self.imageLabel.setPixmap(scaledPixmap)
164 | self.imageLabel.setFixedSize(newSize)
165 | self.imageLabel.adjustSize()
166 |
167 | def closeEvent(self, event):
168 | '''
169 | Communicate to the main window that one image has been closed to remove it from the list of the imageWindows.
170 | '''
171 | self.windowClosing.emit(self)
172 | super().closeEvent(event)
--------------------------------------------------------------------------------
/src/MainWindow.py:
--------------------------------------------------------------------------------
1 | import random
2 | from fileinput import filename
3 | from PyQt5.QtWidgets import *
4 | from PyQt5.QtCore import *
5 | from PyQt5.QtGui import *
6 | from ImageWindow import ImageWindow
7 |
8 | class MainWindow(QMainWindow):
9 | '''
10 | This class manages all the image windows and governs various settings, window count, and the image to be applied on the image windows.
11 | '''
12 | def __init__(self):
13 | '''
14 | Initialize the UI of the window, assign the default path of the images, and intialize an array to store all the image windows.
15 | '''
16 | super().__init__()
17 | self.imageWindows = []
18 | self.currentImagePath = 'Files/tess.gif'
19 | self.initUI()
20 |
21 | def initUI(self):
22 | '''
23 | Initialize the window settings, layout and the UI components.
24 | '''
25 | self.initWindow()
26 | self.initLayout()
27 | self.initComponents()
28 |
29 | def initWindow(self):
30 | '''
31 | The main window settings and properties are assigned here.
32 | The winow location is kept relative to the screen size and the size of the window is goverened by the layout itself.
33 | '''
34 | screen = QApplication.primaryScreen().size()
35 | self.screenWidth = screen.width()
36 | self.screenHeight = screen.height()
37 |
38 | self.move(self.screenWidth // 10, self.screenHeight // 10)
39 |
40 | self.setWindowIcon(QIcon('Files/logo.png'))
41 | self.setWindowTitle('Image Window Manager')
42 |
43 | def initLayout(self):
44 | '''
45 | The layouts that make up the window are intialized here. This includes:
46 | - Central Layout (Widget)
47 | - Left Side Layout (Controls)
48 | - Right Side Layout (Image Display)
49 | - Button Layout (Bottom Buttons)
50 | '''
51 | self.centralWidget = QWidget(self)
52 | self.setCentralWidget(self.centralWidget)
53 | self.mainLayout = QHBoxLayout(self.centralWidget)
54 |
55 | self.leftLayout = QVBoxLayout()
56 | self.leftLayout.setSizeConstraint(QLayout.SetFixedSize)
57 |
58 | self.rightLayout = QVBoxLayout()
59 |
60 | self.btnLayout = QHBoxLayout()
61 | self.btnLayout.addStretch(1)
62 |
63 | def initComponents(self):
64 | '''
65 | Initialize all the UI components that are defined below.
66 | '''
67 | self.initMenuBar()
68 | self.initStatusBar()
69 | self.initToolBar()
70 | self.initScaleSlider()
71 | self.initCheckBoxes()
72 | self.initRadioBtns()
73 | self.initImageDisplay()
74 | self.initBottomButtons()
75 |
76 | def initMenuBar(self):
77 | '''
78 | Initializes a Menu Bar with options to load an image/gif and close all the windows that are open.
79 | '''
80 | menuBar = self.menuBar()
81 | fileMenu = menuBar.addMenu('&File')
82 |
83 | loadAction = QAction('&Open Image/GIF', self)
84 | loadAction.triggered.connect(self.openFileDialog)
85 | fileMenu.addAction(loadAction)
86 |
87 | closeAllAction = QAction('&Close All Windows', self)
88 | closeAllAction.triggered.connect(self.closeAllImageWindows)
89 | fileMenu.addAction(closeAllAction)
90 |
91 | def initStatusBar(self):
92 | '''
93 | Initalize a Status Bar to message about display different operations in the bottom of the screen.
94 | '''
95 | self.statusBar = QStatusBar()
96 | self.setStatusBar(self.statusBar)
97 | self.statusBar.showMessage('Open a new window!', 5000)
98 |
99 | def initToolBar(self):
100 | '''
101 | Initialize a Tool Bar with options to randomize the settings and open multiple windows based on the number of window specified.
102 | '''
103 | toolBar = QToolBar('Tool Bar', self)
104 | self.addToolBar(Qt.TopToolBarArea, toolBar)
105 |
106 | randomizeAction = QAction(QIcon('Files/randomize.png'), 'Randomize', self)
107 | randomizeAction.triggered.connect(self.randomizeSettings)
108 | toolBar.addAction(randomizeAction)
109 |
110 | self.numWindowsInput = QLineEdit(self)
111 | self.numWindowsInput.setValidator(QIntValidator(1, 99))
112 | self.numWindowsInput.setText('5')
113 | self.numWindowsInput.setFixedWidth(self.screenWidth // 50)
114 | toolBar.addWidget(self.numWindowsInput)
115 |
116 | openWindowsBtn = QPushButton('Open Windows', self)
117 | openWindowsBtn.clicked.connect(self.openMultipleWindows)
118 | toolBar.addWidget(openWindowsBtn)
119 |
120 | def initScaleSlider(self):
121 | '''
122 | Initialize a Slider for scaling the image.
123 | The values are scaled 100 folds to enable fine control and each step for a slider is an integer.
124 | '''
125 | self.scaleLabel = QLabel('Scale Image')
126 | self.scaleSlider = QSlider(Qt.Horizontal)
127 |
128 | self.scaleSlider.setFixedSize(self.screenWidth // 15, self.screenHeight // 50)
129 | self.scaleSlider.setRange(25, 400) # Represents .25 to 4
130 | self.scaleSlider.setValue(100) # Represents 1
131 | self.scaleSlider.valueChanged.connect(self.updateImageScale)
132 |
133 | self.leftLayout.addWidget(self.scaleLabel)
134 | self.leftLayout.addWidget(self.scaleSlider)
135 |
136 | def initCheckBoxes(self):
137 | '''
138 | Initilize checkboxes for Image Settings.
139 | '''
140 | self.imageSettLabel = QLabel('Image Settings')
141 | self.moveWWindowCb = QCheckBox('Move with window')
142 | self.keepCenteredCb = QCheckBox('Keep Centered')
143 |
144 | self.leftLayout.addWidget(self.imageSettLabel)
145 | self.leftLayout.addWidget(self.moveWWindowCb)
146 | self.leftLayout.addWidget(self.keepCenteredCb)
147 |
148 | self.moveWWindowCb.clicked.connect(self.updateImageSettings)
149 | self.keepCenteredCb.clicked.connect(self.updateImageSettings)
150 | self.moveWWindowCb.setChecked(True) # Default
151 |
152 | def initRadioBtns(self):
153 | '''
154 | Initialize radio buttons to control the refresh rate of the update function of Image Windows.
155 | '''
156 | self.refreshRateLabel = QLabel('Refresh Rate')
157 | self.slowR = QRadioButton('Slow')
158 | self.medR = QRadioButton('Medium')
159 | self.fastR = QRadioButton('Fast')
160 | self.fastR.setChecked(True) # Default
161 |
162 | self.slowR.clicked.connect(self.updateTimeStep)
163 | self.medR.clicked.connect(self.updateTimeStep)
164 | self.fastR.clicked.connect(self.updateTimeStep)
165 |
166 | self.leftLayout.addWidget(self.refreshRateLabel)
167 | self.leftLayout.addWidget(self.slowR)
168 | self.leftLayout.addWidget(self.medR)
169 | self.leftLayout.addWidget(self.fastR)
170 |
171 | def initImageDisplay(self):
172 | '''
173 | Initialize area for displaying image that will be diplayed in all the Image Windows.
174 | '''
175 | self.imgDisp = QLabel('Display the image here')
176 | self.imgDisp.setAlignment(Qt.AlignCenter)
177 | self.rightLayout.addWidget(self.imgDisp)
178 |
179 | def initBottomButtons(self):
180 | '''
181 | Initialize buttons to open a new Image Window or Quit the application.
182 | '''
183 | self.newWindowBtn = QPushButton('Open Image Window')
184 | self.newWindowBtn.clicked.connect(self.openNewWindow)
185 |
186 | self.quitBtn = QPushButton('Quit')
187 | self.quitBtn.clicked.connect(self.confirmQuit)
188 |
189 | self.btnLayout.addWidget(self.newWindowBtn)
190 | self.btnLayout.addWidget(self.quitBtn)
191 | self.leftLayout.addStretch(1)
192 |
193 | self.mainLayout.addLayout(self.leftLayout, 1)
194 | self.mainLayout.addLayout(self.rightLayout, 1)
195 | self.rightLayout.addLayout(self.btnLayout)
196 |
197 | def openFileDialog(self):
198 | '''
199 | Opens File Explorer to choose image/GIF files.
200 | The selected image is displayed in Image Windows.
201 | '''
202 | self.statusBar.showMessage('Choosing a new Image/GIF', 5000)
203 |
204 | options = QFileDialog.Options()
205 | fileName, i = QFileDialog.getOpenFileName(self, 'Open Image', '', 'Image Files (*.jpg;*.jpeg;*.png;*.bmp;*.gif)', options=options)
206 |
207 | if fileName:
208 | self.currentImagePath = fileName
209 |
210 | self.updateAllWindows()
211 |
212 | def updateAllWindows(self):
213 | '''
214 | Updates image preview in the main window and loads the image in all image windows.
215 | '''
216 | self.displayImagePreview()
217 |
218 | for window in self.imageWindows:
219 | window.loadImage(self.currentImagePath, self.scaleSlider.value() / 100) # Scale = 1
220 |
221 | def openNewWindow(self):
222 | '''
223 | Opens a new image window as a child of the main window.
224 | Connects signals for image position and window closing.
225 | '''
226 | newWindow = ImageWindow(self)
227 |
228 | newWindow.imageMoved.connect(self.onImageMoved)
229 | newWindow.windowClosing.connect(self.removeImageWindow)
230 |
231 | newWindow.show()
232 |
233 | self.imageWindows.append(newWindow)
234 |
235 | newWindow.loadImage(self.currentImagePath, self.scaleSlider.value() / 100)
236 |
237 | self.statusBar.showMessage('Opened new window', 3000)
238 |
239 | for window in self.imageWindows: # Sync GIF
240 | window.restartGif()
241 |
242 | self.displayImagePreview()
243 |
244 | self.updateAllSettings()
245 |
246 | def onImageMoved(self, globalPos):
247 | '''
248 | Handles communication b/w windows on moving a window and updates the position of the images in other windows.
249 | '''
250 | senderWindow = self.sender()
251 |
252 | for window in self.imageWindows:
253 | if window is not senderWindow:
254 | localPos = window.mapFromGlobal(globalPos)
255 | window.imageLabel.move(localPos)
256 | window.update() # Refresh
257 | window.isImageMoved = False
258 |
259 | def displayImagePreview(self):
260 | '''
261 | Display current image in image display area and scale the image to fit the display area while maintaining aspect ratio.
262 | '''
263 | pixmap = QPixmap(self.currentImagePath)
264 |
265 | if not pixmap.isNull():
266 | scaledPixmap = pixmap.scaled(self.imgDisp.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
267 |
268 | self.imgDisp.setPixmap(scaledPixmap)
269 | self.imgDisp.adjustSize()
270 | else:
271 | self.statusBar.showMessage('Failed to load image.', 5000)
272 |
273 | def updateAllSettings(self):
274 | '''
275 | Single method to update all settings from the Main Window.
276 | '''
277 | self.updateImageScale()
278 | self.updateTimeStep()
279 | self.updateImageSettings()
280 |
281 | def updateTimeStep(self):
282 | '''
283 | Update refresh rate of Update Function in the windows based on the radio button selected.
284 | '''
285 | timeStep = 0
286 | if self.fastR.isChecked():
287 | timeStep = 1
288 | elif self.medR.isChecked():
289 | timeStep = 100
290 | else:
291 | timeStep = 2000
292 |
293 | for window in self.imageWindows:
294 | window.updateFunction(timeStep)
295 |
296 | def updateImageScale(self):
297 | '''
298 | Apply scale value from the slider to the all the image and their labels in the image window.
299 | '''
300 | scaleFactor = self.scaleSlider.value() / 100 # Scale = 1
301 |
302 | for window in self.imageWindows:
303 | window.setScale(scaleFactor)
304 |
305 | self.statusBar.showMessage('Scale Value Assigned', 5000)
306 |
307 | def updateImageSettings(self):
308 | '''
309 | Apply settings from the checkboxes.
310 | Give message in the status bar about one of the bugs that we're currently facing.
311 | '''
312 | moveWithWindow = self.moveWWindowCb.isChecked()
313 | keepCentered = self.keepCenteredCb.isChecked()
314 |
315 | for window in self.imageWindows:
316 | window.setMoveWithWindow(moveWithWindow)
317 | window.setKeepCentered(keepCentered)
318 |
319 | if (moveWithWindow == False):
320 | self.statusBar.showMessage('Parent window still moves the image for ref to other windows', 5000) # To be fixed
321 |
322 | def randomizeSettings(self):
323 | '''
324 | Randomize all the Main Window Settings.
325 | '''
326 | self.scaleSlider.setValue(random.randint(25, 400))
327 |
328 | self.moveWWindowCb.setChecked(random.choice([True, False]))
329 | self.keepCenteredCb.setChecked(random.choice([True, False]))
330 |
331 | random.choice([self.slowR, self.medR, self.fastR]).setChecked(True)
332 |
333 | for i in range(random.randint(2, 10)):
334 | self.openNewWindow()
335 |
336 | self.updateAllSettings()
337 |
338 | def openMultipleWindows(self):
339 | '''
340 | Open multiple mindows specified in the toolbar.
341 | '''
342 | numWindows = int(self.numWindowsInput.text())
343 |
344 | for i in range(numWindows):
345 | self.openNewWindow()
346 |
347 | self.statusBar.showMessage('Opened {} windows!'.format(numWindows), 5000)
348 |
349 | def closeAllImageWindows(self):
350 | '''
351 | Close all the windows and remove their reference in the Main Window.
352 | '''
353 | windowsToClose = self.imageWindows.copy()
354 |
355 | for window in windowsToClose:
356 | window.close()
357 |
358 | self.imageWindows.clear()
359 |
360 | def removeImageWindow(self, window):
361 | '''
362 | Remove closed window based on signals from the closing window.
363 | '''
364 | if window in self.imageWindows:
365 | self.imageWindows.remove(window)
366 |
367 | def confirmQuit(self):
368 | '''
369 | Quit Window. You can't see me :-]
370 | '''
371 | self.statusBar.showMessage('Quitting', 5000)
372 | reply = QMessageBox()
373 | reply.setWindowIcon(QIcon('Files/johnwacenwa.png'))
374 | reply.setIcon(QMessageBox.Question)
375 | reply.setText('Are you sure about that?')
376 | reply.setWindowTitle('Confirm Quit')
377 | reply.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
378 | returnVal = reply.exec_()
379 |
380 | if returnVal == QMessageBox.Yes:
381 | QApplication.instance().quit()
--------------------------------------------------------------------------------
/src/MultiWindowTest.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from PyQt5.QtWidgets import QApplication
3 | from MainWindow import MainWindow
4 |
5 | def main():
6 | '''
7 | Creates the Image Window Manager that can be used to open the image windows.
8 | '''
9 | app = QApplication(sys.argv)
10 | main_window = MainWindow()
11 | main_window.show()
12 | sys.exit(app.exec_()) # For clean exit
13 |
14 | main()
--------------------------------------------------------------------------------
/src/requirements.txt:
--------------------------------------------------------------------------------
1 | PyQt5==5.15.10
--------------------------------------------------------------------------------