Search code examples
pythonpyqt5mouse-cursorqcursor

Is there a way to get hotspot information from .cur file using PyQt?


I am working on a PyQt application and one of the functions is to change the cursor style when user opens the app. It is easy to make this work, the only problem is the hotspot information is default to half of the image's width and height and there is not a certainty that all cursor image have their hotspot info just locate on the center of the image. So I want to get these info from a cur file and set these info by calling QWidget's setCursor method. I have no idea how to get those position information using PyQt.

My code is like this:

@staticmethod
def setCursor(widget: QWidget, cursorIconPath: str):
    widget.setCursor(QCursor(QPixmap(cursorIconPath)))

Please note that the cursor resource file is a .cur file, and technically there is a hotspot info in this file. I also found that QCursor have a method hotSpot() to get this info as a QPoint.

I know there may be some way to get it out of PyQt, like a image editor, but it is troublesome because I need to set this hotspot info in my PyQt application every time I want to change the cursor file.

Is there any way to solve my problem? Any help would be appreciated!


Solution

  • When creating the cursor via QPixmap, any hotspot information in the file will be lost, since Qt will treat it as an ICO image (which has an almost identical format). The QCursor.hotSpot() method can only ever return the values you supply in the constructor - or a generic default calculated as roughly width [or height] / 2 / device-pixel-ratio. So your only option here is to extract the values directly from the file and then supply them in the QCursor constructor. Fortunately, this is quite easy to do as the structure of the file is quite easy to parse.

    Below is a basic demo which shows how to achieve this. (Click on the items to test the cursors).

    enter image description here

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    def create_cursor(path):
        curfile = QtCore.QFile(path)
        if curfile.open(QtCore.QIODevice.ReadOnly):
            pixmap = QtGui.QPixmap.fromImage(
                QtGui.QImage.fromData(curfile.readAll(), b'ICO'))
            if not pixmap.isNull():
                curfile.seek(10)
                stream = QtCore.QDataStream(curfile)
                stream.setByteOrder(QtCore.QDataStream.LittleEndian)
                hx = stream.readUInt16()
                hy = stream.readUInt16()
                return QtGui.QCursor(pixmap, hx, hy)
    
    class Window(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            self.button = QtWidgets.QPushButton('Choose Cursors')
            self.button.clicked.connect(self.handleButton)
            self.view = QtWidgets.QListWidget()
            layout = QtWidgets.QVBoxLayout(self)
            layout.addWidget(self.view)
            layout.addWidget(self.button)
            self.view.itemClicked.connect(self.handleItemClicked)
    
        def handleButton(self):
            files = QtWidgets.QFileDialog.getOpenFileNames(
                self, 'Choose Cursors', QtCore.QDir.homePath(),
                'Cursor Files (*.cur)')[0]
            if files:
                self.view.clear()
                for filepath in files:
                    cursor = create_cursor(filepath)
                    if cursor is not None:
                        item = QtWidgets.QListWidgetItem(self.view)
                        item.setIcon(QtGui.QIcon(cursor.pixmap()))
                        item.setText(QtCore.QFileInfo(filepath).baseName())
                        item.setData(QtCore.Qt.UserRole, cursor)
    
        def handleItemClicked(self, item):
            self.setCursor(item.data(QtCore.Qt.UserRole))
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(['Test'])
        window = Window()
        window.setGeometry(600, 100, 250, 350)
        window.show()
        app.exec()