0
votes

I would need some of your expertise concerning GUI and more precisely PyQt4.

Context

I am currently designing a GUI with PyQt. It was previously done with wxPython, but I was kind of force to migrate to Qt due to internal issue.

At some point, I needed to display traditional RTF content, ie including hidden tags such as {\rtf1\ansi\ansicpg1252\deff0\deflang1036\deflangfe1036{\fonttbl{\f0\fswiss\fprq2\fcharset0 Calibri;}{\f1\froman\fprq2\fcharset2 Symbol;}}, \tx360 or \par and so on.

If I'm not mistaking, QTextEdit from PyQt can't "interpret" this RTF and will just display the whole string. But neither did wxPython and I had found a workaround provided by the wxPython community, which was to copy the string to windows clipboard and then paste it in the wanted text widget.

Thus, I had this piece of code:

class rtfClip():

def __init__(self):
    self.CF_RTF = win32clipboard.RegisterClipboardFormat("Rich Text Format")

# Puts 'toPaste' on the clipboard
def setClipboard(self,toPaste):
    cbOpened = False
    # Wait for board availability, then do operations
    while not cbOpened:
        try:
            win32clipboard.OpenClipboard(0)
            cbOpened = True
            win32clipboard.EmptyClipboard() # need to empty, or prev data will stay
            win32clipboard.SetClipboardData(self.CF_RTF, toPaste)
            win32clipboard.CloseClipboard()
        except Exception, err:
            # If access is denied, that means that the clipboard is in use.
            # Keep trying until it's available.
            if err[0] == 5:  #Access Denied
                pass
                #print 'waiting on clipboard...'
                # wait on clipboard because something else has it. we're waiting a
                # random amount of time before we try again so we don't collide again
                time.sleep( random.random()/50 )
            elif err[0] == 1418:  #doesn't have board open
                pass
            elif err[0] == 0:  #open failure
                pass
            else:
                print 'ERROR in Clipboard section of readcomments: %s' % err
                pass


# Save the user's existing clipboard data, if possible. It is unable to save
# copied files, image data, etc; text, HTML, RTF, etc are preserved just fine
def saveClipboard(self):
    cbOpened = False
    while not cbOpened:
        try:
            win32clipboard.OpenClipboard(0)
            cbOpened = True

            self.cbSaved = {}
            rval = win32clipboard.EnumClipboardFormats( 0 )
            while rval != 0:
                #print "Retrieving CB format %d" % rval
                dat = win32clipboard.GetClipboardData( rval )
                if rval == 15:  #CF_HDROP
                    #this'll error, so just give up
                    self.cbSaved = {}
                    win32clipboard.EmptyClipboard()
                    break
                else:
                    self.cbSaved[ rval ] = win32clipboard.GetClipboardData( rval )
                rval = win32clipboard.EnumClipboardFormats( rval )
            win32clipboard.CloseClipboard()
        except Exception, err:
            if err[0] == 5:  #Access Denied
                #print 'waiting on clipboard...'
                time.sleep( random.random()/50 )
                pass
            elif err[0]== 6:
                #print 'clipboard type error, aborting...'
                win32clipboard.CloseClipboard()
                break
            elif err[0] == 1418:  #doesn't have board open
                cbOpened = False
            elif err[0] == 0:  #open failure
                cbOpened = False
            else:
                print 'Error while saving clipboard: %s' % err
                pass

# Restore the user's clipboard, if possible
def restoreClipboard(self):
    cbOpened = False

    # don't wait for the CB if we don't have to
    if len(self.cbSaved) > 0:
        #open clipboard
        while not cbOpened:
            try:
                win32clipboard.OpenClipboard(0)
                win32clipboard.EmptyClipboard()
                cbOpened = True
            except Exception, err:
                if err[0] == 5:  #Access Denied
                    #print 'waiting on clipboard...'
                    time.sleep( random.random()/50 )
                    pass
                elif err[0] == 1418:  #doesn't have board open
                    cbOpened = False
                elif err[0] == 0:  #open failure
                    cbOpened = False
                else:
                    print 'Error with clipboard restoration: %s' % err
                    pass
        #replace items
        try:
            for item in self.cbSaved:
                data = self.cbSaved.get(item)
                # windows appends NULL to most clipboard items, so strip off the NULL
                if data[-1] == '\0':
                    data = data[:-1]
                win32clipboard.SetClipboardData( item, data )
        except Exception, err:
            #print 'ERR: %s' % err
            win32clipboard.EmptyClipboard()
        try:
            win32clipboard.CloseClipboard()
        except:
            pass

And then I just had to paste my RTF string in the associated widget:

    rtf = copy_to_clipboard.rtfClip()
    rtf.saveClipboard()  # Save the current user's clipboard
    rtf.setClipboard(my_rtf_string_full_of_rtf_tags)  # Put our RTF on the clipboard
    preview_dlg = preview_rtf_text(None)
    preview_dlg.preview_rtf_ctrl.SetEditable(True)
    preview_dlg.preview_rtf_ctrl.Paste()  # Paste in into the textbox
    rtf.restoreClipboard()  # Restore the user's clipboard

    preview_dlg.ShowModal()

    preview_dlg.Destroy()

(preview_rtf_text being a class with only a TextCtrl named preview_rtf_ctrl)

Problem

My problem is that for any reason I can't manage to get this solution working with PyQt.

I have attempted designing a very similar solution with

        rtf = copy_to_clipboard.rtfClip()
        rtf.saveClipboard()  # Save the current user's clipboard
        rtf.setClipboard(rtf_content)  # Put our RTF on the clipboard
        #
        rtf_preview_dlg = AEM_RTF_preview(self)
        rtf_preview_dlg.rtf_preview_ctl.setReadOnly(False)
        rtf_preview_dlg.rtf_preview_ctl.setAcceptRichText(True)

        cursor = QtGui.QTextCursor(rtf_preview_dlg.rtf_preview_ctl.document())
        cursor.setPosition(0)
        rtf_preview_dlg.rtf_preview_ctl.setTextCursor(cursor)

        rtf_preview_dlg.rtf_preview_ctl.paste()
        rtf.restoreClipboard()  # Restore the user's clipboard
        rtf_preview_dlg.rtf_preview_ctl.setReadOnly(True)

        rtf_preview_dlg.exec_()

But for any reason this won't work: nothing is pasted to the QTextEdit (rtf_preview_ctl).

I saw on some topics that PyQt had its own clipboard, but how would I make him "take" the content from the windows one? Is it even a solution?

Sorry for the very long question, I hope some of you may have an idea, since it would be an important feature of the GUI.

EDIT : There might be other solution for my need, my dream would just be to display formatted microsoft RTF content, one way or another.

1
Do you need to preserve the rich text formattation in the output? As far as I can tell, the wx.TextCtrl can only paste plain text contents. I only tried copying some formatted text from WordPad, and it seems to paste it as plain text.musicamante
Yes I want to preserve format, and the copy/paste from win32clipboard that I provided totally worked in a wx.TextCtrl, I could view it as if I opened it with Wordpad.ThylowZ
Ok, got it, I forgot to set the wx.TE_RICH2 to the TextCtrl test I made. The reason it works is that most widgets wx uses are abstractions to the platform's native widgets, so you're able to paste rtf contents only because the native widget API that Windows exposes allows you to do that: in fact, with the same code I'm able to paste RTF content if I run it under Wine, but not on Linux. The only solution is to find a valid rtf to x(ht)ml converter (there are many, but most of them have limitations) or make your own parser, as Qt has its own native widgets and never uses the platform ones.musicamante
Ok, thank you for kind answer. Would you recommand any of these converter to me?ThylowZ
Unfortunately I've not been able to find a valid solution. rtf2xml seems to parse correctly the input, but the output is a raw xml which needs XLS conversion to be correctly shown. pyth library doesn't support colored text (and seems to have issues with bullet lists).musicamante

1 Answers

0
votes

I found a very old command line utility, unrtf. It outputs to STDOUT, so we need to process the output from there. It's been made for linux, but Windows binaries are available, even if the version I found is a bit older than the latest provided for Linux. It requires to write a temporary file and might have some small issues with rtf conversion, but for simple cases seems to work fine enough. In this case I automatically detect if there's some rtf content in the clipboard (so you can test it with along with your current program), but you can also paste raw rtf contents there to test it: you actually don't need the clipboard at all to make it work. As far as I can understand it also supports tables and images, which are exported in external files (so you might have to test its behavior and possibly edit the html before actually applying it to the QTextEdit).

rtfTypes = set(['text/rtf', 'text/richtext', 'application/x-qt-windows-mime;value="Rich Text Format"'])

class PasteWidget(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        l = QtWidgets.QGridLayout()
        self.setLayout(l)
        self.input = QtWidgets.QTextEdit()
        l.addWidget(self.input)
        self.input.setAcceptRichText(False)
        self.pasteBtn = QtWidgets.QPushButton('Paste')
        l.addWidget(self.pasteBtn)
        self.pasteBtn.clicked.connect(self.paste)
        self.convertBtn = QtWidgets.QPushButton('Convert')
        l.addWidget(self.convertBtn)
        self.convertBtn.clicked.connect(self.convert)
        self.output = QtWidgets.QTextEdit()
        l.addWidget(self.output)
        self.output.setReadOnly(True)
        self.clipboard = QtWidgets.QApplication.clipboard()
        self.clipboard.changed.connect(self.checkClipboard)
        self.checkClipboard()

    def checkClipboard(self, mode=QtGui.QClipboard.Clipboard):
        if mode != QtGui.QClipboard.Clipboard:
            return
        self.pasteBtn.setEnabled(bool(set(self.clipboard.mimeData().formats()) & rtfTypes))

    def paste(self):
        mime = self.clipboard.mimeData()
        for format in mime.formats():
            if format in rtfTypes:
                self.input.setPlainText(str(mime.data(format)))

    def convert(self):
        rtf = self.input.toPlainText()
        if not rtf:
            return
        tempPath = QtCore.QDir.tempPath()
        with open(os.path.join(tempPath, '_sourceRtf'), 'wb') as _input:
            _input.write(rtf)
        unrtf = QtCore.QProcess()
        unrtf.readyReadStandardOutput.connect(lambda: self.output.setHtml(str(unrtf.readAllStandardOutput())))
        unrtf.start('unrtf.exe', ['--html', os.path.join(tempPath, '_sourceRtf')])

Obviously the unrtf.exe has to be in the system path (or the path of the main script).