Hi! Hope you're enjoying this blog. I have a new home at www.goldsborough.me. Be sure to also check by there for new posts <3
Showing posts with label pyqt. Show all posts
Showing posts with label pyqt. Show all posts

Saturday, September 20, 2014

Building a text editor with PyQt




I have always enjoyed building beautiful Graphical User Interfaces (GUIs) to the back-end computations, number-crunching and algorithms of my programs. For Python, my GUI library of choice is the Python binding for Qt, PyQt. This tutorial will show you how you can use PyQt to build a simple but useful rich-text editor. The first part of the tutorial will focus on the core features and skeleton of the editor. In the second part of the tutorial we'll take care of text-formatting and in the third part we'll add some useful extensions like a find-and-replace dialog, support for tables and more.

Before we get started, two things:


Once you`re set up and ready to go, we can embark on our journey to create a totally awesome text editor.

An empty canvas

We start out with an empty canvas, a bare-minimum PyQt application:

import sys
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt

class Main(QtGui.QMainWindow):

    def __init__(self, parent = None):
        QtGui.QMainWindow.__init__(self,parent)

        self.initUI()

    def initUI(self):

        # x and y coordinates on the screen, width, height
        self.setGeometry(100,100,1030,800)

        self.setWindowTitle("Writer")

def main():

    app = QtGui.QApplication(sys.argv)

    main = Main()
    main.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

The first thing we need to do is import the sys module, which PyQt needs to start our application, as well as all the necessary modules from the PyQt4 package (PyQt5 if you have the newer version).

We'll call our class Main and let it inherit from PyQt's QMainWindow class. In the __init__ method, we initialize the parent class as well as all the UI settings for our application. The latter we just pack into the initUI() method. At the moment, the only settings we need are those concerning position on the screen, the size of the window and the window's title. We can set the first two using the setGeometry() method, which lets us set the x and y coordinates of the window on the screen, the width and lastly its height. We also set our application's window title using the setWindowTitle() method. For simplicity, we'll just call our text editor Writer.

Lastly, we need a main function that takes care of instantiating and displaying our window. We do so by creating a new Main object and calling its show() method.

And there was text

Now that we have a basic PyQt application up and running, we can start making it look more like a text editor:

def initToolbar(self):

  self.toolbar = self.addToolBar("Options")

  # Makes the next toolbar appear underneath this one
  self.addToolBarBreak()

def initFormatbar(self):

  self.formatbar = self.addToolBar("Format")

def initMenubar(self):

  menubar = self.menuBar()

  file = menubar.addMenu("File")
  edit = menubar.addMenu("Edit")
  view = menubar.addMenu("View")

def initUI(self):

    self.text = QtGui.QTextEdit(self)
    self.setCentralWidget(self.text)

    self.initToolbar()
    self.initFormatbar()
    self.initMenubar()

    # Initialize a statusbar for the window
    self.statusbar = self.statusBar()

    # x and y coordinates on the screen, width, height

    self.setGeometry(100,100,1030,800)

    self.setWindowTitle("Writer")

I left out everything that stayed unchanged from the previous code. As you can see in the initUI() function, we first create a QTextEdit object and set it to our window's "central widget". This makes the QTextEdit object take up the window's entire space. Next up, we need to create three more methods: initToolbar(), initFormatbar() and initMenubar(). The first two methods create toolbars that will appear at the top of our window and contain our text editor's features, such as those concerning file management (opening a file, saving a file etc.) or text-formatting. The last method, initMenubar() creates a set of drop-down menus at the top of the screen.
As of now, the methods contain only the code necessary to make them appear.

For the initToolbar() and initFormatbar() methods, this means creating a new toolbar object by calling our window's addToolBar() method and passing it the name of the toolbar we're creating. Note that in the initToolbar() method, we need to also call the addToolBarBreak() method. This makes the next toolbar, the format bar, appear underneath this toolbar. In case of the menu bar, we call the window's menuBar() method and add three menus to it, "File", "Edit" and "View". We'll populate all of these toolbars and menus in a bit.

Lastly, in the initUI() method, we also create a status bar object. This will create a status bar at the bottom of our window.

An icon is worth a thousand words

Before we start injecting some life into our text editor, we're going to need some icons for its various features. If you had a look at the GitHub repository I linked to at the top of this post, you might have noticed that it contains a folder full of icons. I recommend that you download the repo (if you haven't yet) and copy the icons folder into your working directory. The icons are from iconmonstr, completely free and require no attribution.

File management

Now that we have a basic text editor skeleton in place, we can add some meat to the bone. We'll start with the functions concerning file management.

__init__():

def __init__(self, parent = None):
    QtGui.QMainWindow.__init__(self,parent)

    self.filename = ""

    self.initUI()

initToolbar():

def initToolbar(self):

  self.newAction = QtGui.QAction(QtGui.QIcon("icons/new.png"),"New",self)
  self.newAction.setStatusTip("Create a new document from scratch.")
  self.newAction.setShortcut("Ctrl+N")
  self.newAction.triggered.connect(self.new)

  self.openAction = QtGui.QAction(QtGui.QIcon("icons/open.png"),"Open file",self)
  self.openAction.setStatusTip("Open existing document")
  self.openAction.setShortcut("Ctrl+O")
  self.openAction.triggered.connect(self.open)

  self.saveAction = QtGui.QAction(QtGui.QIcon("icons/save.png"),"Save",self)
  self.saveAction.setStatusTip("Save document")
  self.saveAction.setShortcut("Ctrl+S")
  self.saveAction.triggered.connect(self.save)

  self.toolbar = self.addToolBar("Options")

  self.toolbar.addAction(self.newAction)
  self.toolbar.addAction(self.openAction)
  self.toolbar.addAction(self.saveAction)

  self.toolbar.addSeparator()

  # Makes the next toolbar appear underneath this one
  self.addToolBarBreak()

initMenubar():

file.addAction(self.newAction)
file.addAction(self.openAction)
file.addAction(self.saveAction)

Below the initUI() method:

def new(self):

    spawn = Main(self)
    spawn.show()

def open(self):

    # Get filename and show only .writer files
    self.filename = QtGui.QFileDialog.getOpenFileName(self, 'Open File',".","(*.writer)")

    if self.filename:
        with open(self.filename,"rt") as file:
            self.text.setText(file.read())

def save(self):

    # Only open dialog if there is no filename yet
    if not self.filename:
        self.filename = QtGui.QFileDialog.getSaveFileName(self, 'Save File')

    # Append extension if not there yet
    if not self.filename.endswith(".writer"):
        self.filename += ".writer"

    # We just store the contents of the text file along with the
    # format in html, which Qt does in a very nice way for us
    with open(self.filename,"wt") as file:
        file.write(self.text.toHtml())

As you might have noticed, all the actions we'll be creating for our text editor follow the same code pattern:
  • Create a QAction and pass it an icon and a name
  • Create a status tip, which will display a message in the status bar (and a tool tip if you hover the action)
  • Create a shortcut
  • Connect the QAction's triggered signal to a slot function
Once you've done this for the "new", "open" and "save" actions, you can add them to the toolbar, using the toolbar's addAction() method. Make sure you also call the addSeparator() method, which inserts a separator line between toolbar actions. Because these three actions are responsible for file management, we want to add a separator here. Also, we want to add these three actions to the "file" menu, so in the initMenubar() method, we add the three actions to the appropriate menu.
Next up, we need to create the three slot functions that we connected to our action in the initToolbar() method. The new() method is very easy, all it does is create a new instance of our window and call its show() method to display it.

Before we create the last two methods, let me mention that we'll use ".writer" as our text files' extensions. Now, for open(), we need to open PyQt's getOpenFileName dialog. This opens a file dialog which returns the name of the file the user opens. We also pass this method a title for the file dialog, in this case "Open File", the directory to open initially, "." (current directory) and finally a file filter, so that we only show ".writer" files. If the user didn't close or cancel the file dialog, we open the file and set its text to our text editor's current text.

Lastly, the save() method. We first check whether the current file already has a file name associated with it, either because it was opened with the open() method or already saved before, in case of a new text file. If this isn't the case, we open a getSaveFileName dialog which will again return a filename for us, given the user doesn't cancel or close the file dialog. Once we have a file name, we need to check whether the user already entered our extension when saving the file. If not, we add the extension. Finally, we save our file in HTML format (which stores style as well), using the QTextEdit's toHTML() method.

Printing

Next, we'll create some actions for printing and preview-ing our document.

initToolbar():

self.printAction = QtGui.QAction(QtGui.QIcon("icons/print.png"),"Print document",self)
self.printAction.setStatusTip("Print document")
self.printAction.setShortcut("Ctrl+P")
self.printAction.triggered.connect(self.print)

self.previewAction = QtGui.QAction(QtGui.QIcon("icons/preview.png"),"Page view",self)
self.previewAction.setStatusTip("Preview page before printing")
self.previewAction.setShortcut("Ctrl+Shift+P")
self.previewAction.triggered.connect(self.preview)

Further below:

self.toolbar.addAction(self.printAction)
self.toolbar.addAction(self.previewAction)

self.toolbar.addSeparator()

initMenubar():

file.addAction(self.printAction)
file.addAction(self.previewAction)

Below the initUI() method:

def preview(self):

    # Open preview dialog
    preview = QtGui.QPrintPreviewDialog()

    # If a print is requested, open print dialog
    preview.paintRequested.connect(lambda p: self.text.print_(p))

    preview.exec_()

def print(self):

    # Open printing dialog
    dialog = QtGui.QPrintDialog()

    if dialog.exec_() == QtGui.QDialog.Accepted:
        self.text.document().print_(dialog.printer())

We create the actions following the same scheme as we did for the file management actions and add them to our toolbar as well as the "file" menu. The preview() method opens a QPrintPreviewDialog and optionally prints the document, if the user wishes to do so. The print() method opens a QPrintDialog and prints the document if the user accepts.

Copy and paste - undo and redo

These actions will let us copy, cut and paste text as well as undo/redo actions:

initToolbar():

self.cutAction = QtGui.QAction(QtGui.QIcon("icons/cut.png"),"Cut to clipboard",self)
self.cutAction.setStatusTip("Delete and copy text to clipboard")
self.cutAction.setShortcut("Ctrl+X")
self.cutAction.triggered.connect(self.text.cut)

self.copyAction = QtGui.QAction(QtGui.QIcon("icons/copy.png"),"Copy to clipboard",self)
self.copyAction.setStatusTip("Copy text to clipboard")
self.copyAction.setShortcut("Ctrl+C")
self.copyAction.triggered.connect(self.text.copy)

self.pasteAction = QtGui.QAction(QtGui.QIcon("icons/paste.png"),"Paste from clipboard",self)
self.pasteAction.setStatusTip("Paste text from clipboard")
self.pasteAction.setShortcut("Ctrl+V")
self.pasteAction.triggered.connect(self.text.paste)

self.undoAction = QtGui.QAction(QtGui.QIcon("icons/undo.png"),"Undo last action",self)
self.undoAction.setStatusTip("Undo last action")
self.undoAction.setShortcut("Ctrl+Z")
self.undoAction.triggered.connect(self.text.undo)

self.redoAction = QtGui.QAction(QtGui.QIcon("icons/redo.png"),"Redo last undone thing",self)
self.redoAction.setStatusTip("Redo last undone thing")
self.redoAction.setShortcut("Ctrl+Y")
self.redoAction.triggered.connect(self.text.redo)

Further below:

self.toolbar.addAction(self.cutAction)
self.toolbar.addAction(self.copyAction)
self.toolbar.addAction(self.pasteAction)
self.toolbar.addAction(self.undoAction)
self.toolbar.addAction(self.redoAction)

self.toolbar.addSeparator()

initMenubar():

edit.addAction(self.undoAction)
edit.addAction(self.redoAction)
edit.addAction(self.cutAction)
edit.addAction(self.copyAction)
edit.addAction(self.pasteAction)

As you can see, we don't need any separate slot functions for these actions, as our QTextEdit object already has very handy methods for all of these actions. Note that in the initMenubar() method, we add these actions to the "Edit" menu and not the "File" menu.

Lists

Finally, we'll add two actions for inserting lists. One for numbered lists and one for bulleted lists:

initToolbar():

bulletAction = QtGui.QAction(QtGui.QIcon("icons/bullet.png"),"Insert bullet List",self)
bulletAction.setStatusTip("Insert bullet list")
bulletAction.setShortcut("Ctrl+Shift+B")
bulletAction.triggered.connect(self.bulletList)

numberedAction = QtGui.QAction(QtGui.QIcon("icons/number.png"),"Insert numbered List",self)
numberedAction.setStatusTip("Insert numbered list")
numberedAction.setShortcut("Ctrl+Shift+L")
numberedAction.triggered.connect(self.numberList)

Further below:

self.toolbar.addAction(bulletAction)
self.toolbar.addAction(numberedAction)

Below the initUI() method:

    def bulletList(self):

        cursor = self.text.textCursor()

        # Insert bulleted list
        cursor.insertList(QtGui.QTextListFormat.ListDisc)

    def numberList(self):

        cursor = self.text.textCursor()

        # Insert list with numbers
        cursor.insertList(QtGui.QTextListFormat.ListDecimal)

As you can see, we don't make these actions class members because we don't need to access them anywhere else in our code, we only need to create and use them within the scope of initToolbar().
Concerning the slot functions, we retrieve our QTextEdit's QTextCursor, which has a lot of very useful methods, such insertList(), which, well, does what it's supposed to do. In case of bulletList(), we insert a list with the QTextListFormat set to ListDisc. For numberList(), we insert a list with ListDecimal format.

Final changes

To finish off this part of Building a text editor with PyQt, let's make some final changes in the initUI() method:

self.text.setTabStopWidth(33)


Because PyQt's tab width is very strange, I recommend you set the QTextEdit's "tab stop width" to 33 pixels, which is around 8 spaces.

self.setWindowIcon(QtGui.QIcon("icons/icon.png"))


Now that we have icons, we can add an icon for our window.

self.text.cursorPositionChanged.connect(self.cursorPosition


By connecting our QTextEdit's cursorPositionChanged signal to a function, we can display the cursor's current line and column number in the status bar. Here is the corresponding slot function, below initUI():

def cursorPosition(self):

    cursor = self.text.textCursor()

    # Mortals like 1-indexed things
    line = cursor.blockNumber() + 1
    col = cursor.columnNumber()

    self.statusbar.showMessage("Line: {} | Column: {}".format(line,col))

We first retrieve our QTextEdit's QTextCursor, then grab the cursor's column and block/line number and finally display these numbers in the status bar.

That'll be it for this part of the series. See you soon.

Originally published on BinPress.

Wednesday, September 18, 2013

Dynamically adding objects in PyQt

Hi all, today I thought I'd show you how to dynamically add objects. It may already be clear to you, but it wasn't to me in the beginning, so this is for those struggling with the idea.

We will start out with a simple window, a button, a global variable and a grid layout (important!).

import sys
from PyQt4 import QtGui, QtCore


count = 1

class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.initUI()

    def initUI(self):

        centralwidget = QtGui.QWidget()

        self.add = QtGui.QPushButton("Add")
        

        self.grid = QtGui.QGridLayout()
        
        self.grid.addWidget(self.add,0,0)

        centralwidget.setLayout(self.grid)

        self.setCentralWidget(centralwidget)


    #---------Window settings --------------------------------
        
        self.setGeometry(300,300,280,170)
        self.setWindowTitle("")
        self.setWindowIcon(QtGui.QIcon(""))
        self.setStyleSheet("background-color:")
        self.show()

def main():
    app = QtGui.QApplication(sys.argv)
    main = Main()
    main.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()


Next, we will write the function connecting to the add button. This function will create a new button and add it to the grid. So, first:


self.add.clicked.connect(self.Add)

Then, two functions:

    def Add(self):
        global count

        b = QtGui.QPushButton(str(count),self)
        b.clicked.connect(self.Button)

        self.grid.addWidget(b,count,0)

        count += 1

    def Button(self):
        pass


Don't worry about the Button function for now, I just created it in order to prevent an error. So what exactly happens in the Add function? First, we import(?) the global variable count, which we will need for a couple of things. Then, we create a button and name it anything we want, this is not important, what is important though, is that we set the text to the string encoded integer count, so that each time we add a button and increase the count number, we have a different text.  Of course, this could be anything you want, if, for example, you have a user input something, you could just set it to the variable holding the input. Anyway, connect that button to a function and add it to the grid layout. Here, the variable count is very important, since we need a different position for each button added. Lastly, add 1 to count.

Now the Button function:

    def Button(self):
        
        sender = self.sender()

        print(sender.text())

If you don't know already, to determine the sender of the signal for this slot, you can call the sender() function. This of course just gives us the hexadecimal position and identity of the widget. Since it is a button, we can access it's text to distinguish between them. So call sender.text(), and you'll see which button is being pressed.

This is it for this tutorial, in another tutorial, I'll might cover how to delete them dynamically using a context menu :)

Thursday, September 5, 2013

Fully functional PyQt address book

Hi, just wanted to post the code for a program I did a little while ago. It's an address book that let's you store some data about a person. The buttons are in tile-style and a button get's added dynamically when you create a new contact. Have fun.

import sys,pickle,time
from PyQt4 import QtGui, QtCore


f = open("contacts.txt","rb")
try:
    contacts = pickle.loads(f.read())
    print("1")
except:
    print("2")
    contacts = {}
finally:
    f.close()

class New(QtGui.QDialog):

    def __init__(self,parent=None):
        global nvar,cvar,wvar,ovar
        
        QtGui.QDialog.__init__(self,parent)

        self.initUI()

        nvar = False
        cvar = False
        wvar = False
        ovar = False

    def initUI(self):

        self.name = QtGui.QPushButton("Name",self)
        self.name.clicked.connect(self.Name)

        self.fname = QtGui.QLabel("First name",self)
        self.fnameline = QtGui.QLineEdit(self)

        self.lname = QtGui.QLabel("Last name",self)
        self.lnameline = QtGui.QLineEdit(self)

        self.contact = QtGui.QPushButton("Contact",self)
        self.contact.clicked.connect(self.Contact)

        self.num1 = QtGui.QLabel("Telephone number 1",self)
        self.numline1 = QtGui.QLineEdit(self)

        self.numtype1 = QtGui.QComboBox(self)

        self.numtype1.addItem("Mobile")
        self.numtype1.addItem("Work")
        self.numtype1.addItem("Home")
        self.numtype1.addItem("Fax")

        self.num2 = QtGui.QLabel("Telephone number 2",self)
        self.numline2 = QtGui.QLineEdit(self)

        self.numtype2 = QtGui.QComboBox(self)

        self.numtype2.addItem("Mobile")
        self.numtype2.addItem("Work")
        self.numtype2.addItem("Home")
        self.numtype2.addItem("Fax")

        self.email = QtGui.QLabel("Email",self)
        self.emailline = QtGui.QLineEdit(self)

        self.work = QtGui.QPushButton("Work",self)
        self.work.clicked.connect(self.Work)

        self.title = QtGui.QLabel("Title",self)
        self.titleline = QtGui.QLineEdit(self)

        self.company = QtGui.QLabel("Company",self)
        self.companyline = QtGui.QLineEdit(self)

        self.position = QtGui.QLabel("Position",self)
        self.positionline = QtGui.QLineEdit(self)

        self.compsite = QtGui.QLabel("Company Website",self)
        self.compsiteline = QtGui.QLineEdit(self)

        self.other = QtGui.QPushButton("Other",self)
        self.other.clicked.connect(self.Other)

        self.address = QtGui.QLabel("Address",self)
        self.addressline = QtGui.QLineEdit(self)

        self.website = QtGui.QLabel("Website",self)
        self.websiteline = QtGui.QLineEdit(self)

        self.birthday = QtGui.QLabel("Birthday",self)
        self.birthdayline = QtGui.QDateEdit(self)

        self.notes = QtGui.QLabel("Notes",self)
        self.notesline = QtGui.QTextEdit(self)

        self.save = QtGui.QPushButton("Save",self)
        self.save.clicked.connect(self.Save)
        
        self.cancel = QtGui.QPushButton("Cancel",self)
        self.cancel.clicked.connect(lambda: self.hide())

        sub = [self.fname,self.fnameline,self.lname,self.lnameline,
               self.num1,self.numline1,self.numtype1,self.num2,
               self.numline2,self.numtype2,self.email,self.emailline,
               self.title,self.titleline,self.company,
               self.companyline,self.position,self.positionline,self.compsite,
               self.compsiteline,self.address,self.addressline,self.website,
               self.websiteline,self.birthday,self.birthdayline,self.notes,
               self.notesline]

        main = [self.name,self.contact,self.work,self.other]

        widgets = [self.name,self.fname,self.fnameline,self.lname,self.lnameline,
                 self.contact,self.num1,self.numline1,self.numtype1,self.num2,
                 self.numline2,self.numtype2,self.email,self.emailline,
                 self.work,self.title,self.titleline,self.company,
                 self.companyline,self.position,self.positionline,self.compsite,
                 self.compsiteline,self.other,self.address,self.addressline,self.website,
               self.websiteline,self.birthday,self.birthdayline,self.notes,
               self.notesline]

        for i in sub:
            i.hide()

        grid = QtGui.QGridLayout(self)

        pos = 0

        for i in widgets:
            grid.addWidget(i,pos,0,1,2)
            pos +=1

        grid.addWidget(self.save,pos,0,1,1)
        grid.addWidget(self.cancel,pos,1,1,1)

        self.setLayout(grid)

        self.setGeometry(300,200,175,100)
        self.setWindowTitle("Add contact")
        self.setStyleSheet("font-size:13px")

    def Name(self):
        global nvar
        
        if nvar == False:
            self.fname.show()
            self.fnameline.show()
            self.lname.show()
            self.lnameline.show()

            nvar = True
            
        else:
            self.fname.hide()
            self.fnameline.hide()
            self.lname.hide()
            self.lnameline.hide()

            nvar = False

        self.resize(175,100)

    def Contact(self):
        global cvar
        
        if cvar == False:
            self.num1.show()
            self.numline1.show()
            self.numtype1.show()
            self.num2.show()
            self.numline2.show()
            self.numtype2.show()
            self.email.show()
            self.emailline.show()

            cvar = True
            
        else:
            self.num1.hide()
            self.numline1.hide()
            self.numtype1.hide()
            self.num2.hide()
            self.numline2.hide()
            self.numtype2.hide()
            self.email.hide()
            self.emailline.hide()

            cvar = False

        self.resize(175,100)

    def Work(self):
        global wvar
        
        if wvar == False:
            self.title.show()
            self.titleline.show()
            self.company.show()
            self.companyline.show()
            self.position.show()
            self.positionline.show()
            self.compsite.show()
            self.compsiteline.show()

            wvar = True
            
        else:
            self.title.hide()
            self.titleline.hide()
            self.company.hide()
            self.companyline.hide()
            self.position.hide()
            self.positionline.hide()
            self.compsite.hide()
            self.compsiteline.hide()

            wvar = False

        self.resize(175,100)

    def Other(self):
        global ovar

        if ovar == False:
            self.address.show()
            self.addressline.show()
            self.website.show()
            self.websiteline.show()
            self.birthday.show()
            self.birthdayline.show()
            self.notes.show()
            self.notesline.show()

            ovar = True

        else:
            self.address.hide()
            self.addressline.hide()
            self.website.hide()
            self.websiteline.hide()
            self.birthday.hide()
            self.birthdayline.hide()
            self.notes.hide()
            self.notesline.hide()

            ovar = False

        self.resize(175,100)


    def Save(self):
        global contacts,button

        name = self.fnameline.text() + " " + self.lnameline.text()
        fname = self.fnameline.text()
        lname = self.lnameline.text()
        num1 = self.numline1.text()
        numtype1 = self.numtype1.currentIndex()
        num2 = self.numline2.text()
        numtype2 = self.numtype2.currentIndex()
        email = self.emailline.text()
        title = self.titleline.text()
        company = self.companyline.text()
        position = self.positionline.text()
        compsite = self.compsiteline.text()
        address = self.addressline.text()
        birthday = self.birthdayline.date()
        notes = self.notesline.toPlainText()

        if button == "+":
            contacts[name] = {"First name":fname,
                              "Last name":lname,
                              "Telephone number 1":num1,
                              "Type 1":numtype1,
                              "Telephone number 2":num2,
                              "Type 2":numtype2,
                              "Email":email,
                              "Title":title,
                              "Company":company,
                              "Position":position,
                              "Company Website":compsite,
                              "Address":address,
                              "Birthday":birthday,
                              "Notes":notes
                              }
        else:
            del contacts[button]
            contacts[name] = {"First name":fname,
                              "Last name":lname,
                              "Telephone number 1":num1,
                              "Type 1":numtype1,
                              "Telephone number 2":num2,
                              "Type 2":numtype2,
                              "Email":email,
                              "Title":title,
                              "Company":company,
                              "Position":position,
                              "Company Website":compsite,
                              "Address":address,
                              "Birthday":birthday,
                              "Notes":notes
                              }

        f = open("contacts.txt","wb")
        pickle.dump(contacts,f)
        f.close()
        
        self.close()
        
class Main(QtGui.QMainWindow):

    def __init__(self,parent=None):
        QtGui.QMainWindow.__init__(self,parent)
        self.initUI()

    def initUI(self):
        global contacts
        
        centralwidget = QtGui.QWidget()

        self.timer = QtCore.QTimer(self)
        self.timer.start(10)
        self.timer.timeout.connect(self.Hover)

        self.add = QtGui.QPushButton("+",self)
        self.add.setStyleSheet("font-size:40px;background-color:#333333;border: 2px solid #222222")
        self.add.setFixedSize(100,100)

        self.add.clicked.connect(self.Add)

        self.grid = QtGui.QGridLayout()

        self.grid.addWidget(self.add,0,0)
        
        centralwidget.setLayout(self.grid)

        if contacts:
            self.addTile()

        self.setCentralWidget(centralwidget)
     
#---------Window settings --------------------------------
        
        self.setGeometry(300,300,500,100)
        self.setWindowTitle("PyTact")

    def clickContact(self):
        global contacts, button

        sender = self.sender()
        
        ind = self.sender().text().index("\n")
        button = self.sender().text()[:ind] + self.sender().text()[ind+1:]
        
        contact_id = contacts[button]

        self.timer.start(150)
        sender.setStyleSheet("font-size:15px;background-color:#666666;border: 2px solid #555555")

        new = New(self)

        fname = contact_id["First name"]
        new.fnameline.setText(fname)
        lname = contact_id["Last name"]
        new.lnameline.setText(lname)
        num1 = contact_id["Telephone number 1"]
        new.numline1.setText(num1)
        numtype1 = contact_id["Type 1"]
        new.numtype1.setCurrentIndex(numtype1)
        num2 = contact_id["Telephone number 2"]
        new.numline2.setText(num2)
        numtype2 = contact_id["Type 2"]
        new.numtype2.setCurrentIndex(numtype2)
        email = contact_id["Email"]
        new.emailline.setText(email)
        title = contact_id["Title"]
        new.titleline.setText(title)
        company = contact_id["Company"]
        new.companyline.setText(company)
        position = contact_id["Position"]
        new.positionline.setText(position)
        compsite = contact_id["Company Website"]
        new.compsiteline.setText(compsite)
        address = contact_id["Address"]
        new.addressline.setText(address)
        birthday = contact_id["Birthday"]
        new.birthdayline.setDate(birthday)
        notes = contact_id["Notes"]
        new.notesline.setText(notes)

        new.show()

        new.save.clicked.connect(self.addTile)

    def ContextMenu(self):
        global sender
        sender = self.sender()
        
        self.menu = QtGui.QMenu(self)

        remove = QtGui.QAction("Remove",self)
        remove.triggered.connect(self.Remove)

        self.menu.addAction(remove)

        self.menu.show()

    def Remove(self):
        global sender
        global contacts

        del contacts[sender.text()]

        sender.setParent(None)
        
    def addTile(self):
        global contacts
        
        for i in reversed(range(self.grid.count())):
            self.grid.itemAt(i).widget().setParent(None)

        self.grid.addWidget(self.add,0,0)

        h = 1
        v = 0

        for i in contacts.keys():

            ind = i.rindex(" ")
            t = i[:ind]+ "\n" + i[ind:]
            
            b = QtGui.QPushButton(t,self)
            b.setStyleSheet("color:#0191C8;font-size:15px;background-color:#333333;border: 2px solid #222222")
            b.setFixedSize(100,100)
            b.clicked.connect(self.clickContact)

            b.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
            b.customContextMenuRequested.connect(self.ContextMenu)

            self.grid.addWidget(b,v,h)

            h += 1

            if h > 3 and h % 4 == 0:
                v += 1
                h = 0

    def Add(self):
        global button
        
        self.timer.start(150)
        self.add.setStyleSheet("color:#0191C8;font-size:40px;background-color:#666666;border: 2px solid #555555")

        button = self.sender().text()
        
        new = New(self)
        new.show()

        new.save.clicked.connect(self.addTile)

    def Hover(self):
        self.timer.start(10)
        for i in reversed(range(self.grid.count())):
            if i > 0:
                if self.grid.itemAt(i).widget().underMouse() == True:
                    self.grid.itemAt(i).widget().setStyleSheet("color:#0191C8;font-size:15px;background-color:#444444;border: 2px solid #333333")
                else:
                    self.grid.itemAt(i).widget().setStyleSheet("color:#0191C8;font-size:15px;background-color:#333333;border: 2px solid #222222")
            else:
                if self.add.underMouse() == True:
                    self.add.setStyleSheet("color:#0191C8;font-size:40px;background-color:#444444;border: 2px solid #333333")
                else:
                    self.add.setStyleSheet("color:#0191C8;font-size:40px;background-color:#333333;border: 2px solid #222222")


def main():
    app = QtGui.QApplication(sys.argv)
    main= Main()
    main.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

Thursday, August 22, 2013

Tutorial: How to make a flat,square and colored button in PyQt

Hi there, today I want to show you how to make a square button in PyQt. You may think this is easily done, but it's not as obvious as it seems.

It will look like this:


I really like the new design Microsoft has come up with, so I decided to try to implement it in PyQt. The problem is, when you set a color for your button using StyleSheets, the button becomes flat and loses everything a user is used to when using a button. What I mean is, when you hover a button, you are used to it changing the color tone a bit, same thing when clicking it, so I had to come up with a way of changing the color manually. Here is how you first of all make a window with a button that is square and colored:

import sys
from PyQt4 import QtGui,QtCore

class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.initUI()

    def initUI(self):
        
        centralwidget = QtGui.QWidget()

        self.button = QtGui.QPushButton("+",self)
        self.button.setStyleSheet("font-size:40px;background-color:#333333;\
        border: 2px solid #222222")
        self.button.setFixedSize(100,100)
        self.button.clicked.connect(self.Button)

        self.grid = QtGui.QGridLayout()

        self.grid.addWidget(self.button,0,0)
        
        centralwidget.setLayout(self.grid)

        self.setCentralWidget(centralwidget)
     
#---------Window settings --------------------------------
        
        self.setGeometry(300,300,500,100)

def Button(self):
        print("Clicked")

def main():
    app = QtGui.QApplication(sys.argv)
    main= Main()
    main.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()


In PyQt we can change many design elements using stylesheets, as one would do when creating a website. Here, I change the button's font-size to 40px, the background color to #333333 and the border to  2px wide solid and its color to #222222. If you don't know about CSS, color is usually displayed using their hexadecimal values, but you can also use their plain names, although there are only limited of those. You can find a nice list of these plain colors and their hexadecimal values here , and if you want to change other elements I recommend you check out w3school's CSS tutorials here.

If you try the above code, you will see that the button does not react to anything you're used to from a GUI button. Of course, everything you are "used to", is just clever use of shadow and change of color tone. So, first, we will have to have a way of checking whether or not the button is being hovered. I've researched many different ways of doing so, and came up with my own.

In PyQt, a QWidget has the attribute underMouse(), which basically checks if the widget is under a cursor. The problem is, we have to have this checked permanently throughout the program. For this, we use a QTimer, that connects via its timeout signal to a function every 0,01 seconds, or 10 milliseconds. In this function, we check if the button is underMouse, or not, and change color tone and shadow accordingly.

So, create a QTimer:

self.timer = QtCore.QTimer(self)
self.timer.start(10)
self.timer.timeout.connect(self.Hover)

As well as a function Hover, to which the QTimer connects once it times out. As described above, in this function, we check if the button is under the cursor and change the tone:

def Hover(self):
        self.timer.start(10)
        if self.button.underMouse() == True:
            self.button.setStyleSheet("font-size:40px;background-color:#444444;\
            border: 2px solid #333333")
        else:
            self.button.setStyleSheet("font-size:40px;background-color:#333333;\
            border: 2px solid #222222")

As you can see, when the button is being hovered I change the color to #444444, that's one tone lighter than the original color (#000000 is black, #111111 is one tone lighter; #FFFFFF is white, #EEEEEE is one tone darker etc.), also the border color is changed to a lighter tone. You see, when the border is darker than the color of the button, it makes the button seem to "stick out". If the button isn't hovered we change it back to the original colors. In the first line, I start the timer again at 10 milliseconds, as to reset it, since it would otherwise just time out the first time and then never again.

Lastly, we also want the button to have a different tone when it is clicked, so inside the Button function, we add another change to the style sheet:

def Button(self):
        print("Clicked")
        self.timer.start(150)
        self.add.setStyleSheet("font-size:40px;background-color:#666666;\
        border: 2px solid #555555")


You may have noticed that I restarted the timer, now timing out at 150 milliseconds. We do this so that the button has the "clicked color" for longer than just 10 milliseconds, which is when the timer would time out normally, thus making the change practically invisible to the eye. When the timer times out this time, it connects to the Hover function again, where we set the timer to start at 10 milliseconds.

There you go, that's more or less how to "design" a colored and flat looking button. You can change the color to any other now, just don't forget to change the tone to one lighter in the Hover function.

Saturday, August 17, 2013

PyQt Stopwatch and Timer

Made these two in about an hour, just wanted to share with you ☺

Stopwatch:


import sys
from PyQt4 import QtGui, QtCore

s = 0
m = 0
h = 0

class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.initUI()

    def initUI(self):

        centralwidget = QtGui.QWidget(self)

        self.lcd = QtGui.QLCDNumber(self)

        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.Time)

        self.start = QtGui.QPushButton("Start",self)
        self.start.clicked.connect(self.Start)

        self.stop = QtGui.QPushButton("Stop",self)
        self.stop.clicked.connect(lambda: self.timer.stop())

        self.reset = QtGui.QPushButton("Reset",self)
        self.reset.clicked.connect(self.Reset)

        grid = QtGui.QGridLayout()
        
        grid.addWidget(self.start,1,0)
        grid.addWidget(self.stop,1,1)
        grid.addWidget(self.reset,1,2)
        grid.addWidget(self.lcd,2,0,1,3)

        centralwidget.setLayout(grid)

        self.setCentralWidget(centralwidget)

#---------Window settings --------------------------------
        
        self.setGeometry(300,300,280,170)

    def Reset(self):
        global s,m,h

        self.timer.stop()

        s = 0
        m = 0
        h = 0

        time = "{0}:{1}:{2}".format(h,m,s)

        self.lcd.setDigitCount(len(time))
        self.lcd.display(time)

    def Start(self):
        global s,m,h
        
        self.timer.start(1000)
    
    def Time(self):
        global s,m,h

        if s < 59:
            s += 1
        else:
            if m < 59:
                s = 0
                m += 1
            elif m == 59 and h < 24:
                h += 1
                m = 0
                s = 0 
            else:
                self.timer.stop()

        time = "{0}:{1}:{2}".format(h,m,s)

        self.lcd.setDigitCount(len(time))
        self.lcd.display(time)

def main():
    app = QtGui.QApplication(sys.argv)
    main= Main()
    main.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()



Timer:


import sys
from PyQt4 import QtGui, QtCore

class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.initUI()

    def initUI(self):

        self.timer = QtCore.QTimer(self)

        self.lcd = QtGui.QLCDNumber(self)
        self.lcd.setDigitCount(8)

        self.time = QtGui.QTimeEdit(self)
        self.timer.timeout.connect(self.Time)

        self.set = QtGui.QPushButton("Set",self)
        self.set.clicked.connect(self.Set)

        self.stop = QtGui.QPushButton("Stop",self)
        self.stop.clicked.connect(lambda: self.timer.stop())

        grid = QtGui.QGridLayout(self)

        grid.addWidget(self.lcd,3,0)
        grid.addWidget(self.time,0,0)
        grid.addWidget(self.set,1,0)
        grid.addWidget(self.stop,2,0)

        centralwidget = QtGui.QWidget()

        self.setCentralWidget(centralwidget)

        centralwidget.setLayout(grid)

#---------Window settings --------------------------------
        
        self.setGeometry(300,300,280,170)

    def Set(self):
        global t,h,m,s
        
        t = self.time.time()
        self.lcd.display(t.toString())

        self.timer.start(1000)

        h = t.hour()
        m = t.minute()
        s = t.second()

    def Time(self):
        global t,h,m,s

        if s > 0:
            s -=1
        else:
            if m > 0:
                m -= 1
                s = 59
            elif m == 0 and h > 0:
                h -= 1
                m = 59
                s = 59
            else:
                self.timer.stop()

                stop = QtGui.QMessageBox.warning(self,"Time is up","Time is up")

        time = ("{0}:{1}:{2}".format(h,m,s))

        self.lcd.setDigitCount(len(time))
        self.lcd.display(time)

def main():
    app = QtGui.QApplication(sys.argv)
    main= Main()
    main.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()



Fully functional PyQt Email App

Hey there. In my last post I showed you how to send an email in Python, and in this one I'm just going to share a program with you that I made. It's basically a GUI email sender. I made a login window that shows up first and connects to the smpt server (I have radio buttons for Windows Mail, Yahoo Mail and Google Mail) and then the main program where you send the email. The hardest part was implementing a way of attaching files and also displaying them in the window, like if you attach a .doc file, it shows the image of a file with ".doc" on it. A nice thing is however if you attach a .png image, it shows a miniature version of it.

Have fun:


import sys
import os
import time

import smtplib
import mimetypes

from email import encoders
from email.utils import formatdate
from email.message import Message
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

from PyQt4 import QtGui, QtCore

attachments = []
labels = []

class Login(QtGui.QDialog):
    def __init__(self,parent = None):
        QtGui.QDialog.__init__(self, parent)
        
        self.initUI()

    def initUI(self):

        self.live = QtGui.QRadioButton("Windows Live",self)
        self.gmail = QtGui.QRadioButton("Google Mail",self)
        self.yahoo = QtGui.QRadioButton("Yahoo! Mail",self)

        e = [self.live,self.gmail,self.yahoo]

        for i in e:
            i.clicked.connect(self.Email)

        self.userl = QtGui.QLabel("E-mail:",self)
        self.user = QtGui.QLineEdit(self)

        self.passl = QtGui.QLabel("Password:",self)
        
        self.passw = QtGui.QLineEdit(self)
        self.passw.setEchoMode(self.passw.Password)

        self.echo = QtGui.QCheckBox("Show/Hide password",self)
        self.echo.stateChanged.connect(self.Echo)

        self.go = QtGui.QPushButton("Login",self)
        self.go.clicked.connect(self.Login)

        grid = QtGui.QGridLayout()

        grid.addWidget(self.live,0,0)
        grid.addWidget(self.gmail,0,1)
        grid.addWidget(self.yahoo,0,2)
        grid.addWidget(self.userl,1,0,1,1)
        grid.addWidget(self.user,1,1,1,2)
        grid.addWidget(self.passw,2,1,1,2)
        grid.addWidget(self.passl,2,0,1,1)
        grid.addWidget(self.echo,3,0,1,2)
        grid.addWidget(self.go,3,2)

        self.setLayout(grid)

        self.setGeometry(300,300,350,200)
        self.setWindowTitle("PyMail Login")
        self.setWindowIcon(QtGui.QIcon("PyMail"))
        self.setStyleSheet("font-size:15px;")

    def Echo(self,state):
        if state == QtCore.Qt.Checked:
            self.passw.setEchoMode(self.passw.Normal)
        else:
            self.passw.setEchoMode(self.passw.Password)

    def Email(self):
        global account
        account = self.sender().text()

    def Login(self):
        global account
        global server
        global user

        user = self.user.text()

        if account == "Windows Live":
            server = smtplib.SMTP('smtp.live.com',25)

        elif account == "Google Mail":
            server = smtplib.SMTP('smtp.gmail.com',25)

        elif account == "Yahoo! Mail":
            server = smtplib.SMTP('smtp.mail.yahoo.com',465)

        try:    
            server.ehlo()
            server.starttls()
            server.ehlo()
            server.login(user, self.passw.text())

            self.hide()

            main = Main(self)
            main.show()
            
        except smtplib.SMTPException:
            msg = QtGui.QMessageBox.critical(self, 'Login Failed',
            "Username/Password combination incorrect", QtGui.QMessageBox.Ok | 
            QtGui.QMessageBox.Retry, QtGui.QMessageBox.Ok)

            if msg == QtGui.QMessageBox.Retry:
                self.Login()
        
class Main(QtGui.QMainWindow):

    def __init__(self,parent=None):
        QtGui.QMainWindow.__init__(self,parent)
        self.initUI()

    def initUI(self):
        global user

        self.send = QtGui.QPushButton("Send",self)
        self.send.clicked.connect(self.Send)

        self.from_label = QtGui.QLabel("From",self)

        self.to_label = QtGui.QLabel("To",self)

        self.subject_label = QtGui.QLabel("Subject",self)

        self.from_addr = QtGui.QLineEdit(self)
        self.from_addr.setText(user)

        self.to_addr = QtGui.QLineEdit(self)
        self.to_addr.setPlaceholderText("godfather@corleone.it")

        self.subject = QtGui.QLineEdit(self)
        self.subject.setPlaceholderText("I got an offer you can't refuse")

        self.image = QtGui.QPushButton("Attach file",self)
        self.image.clicked.connect(self.Image)
        
        self.text = QtGui.QTextEdit(self)

        centralwidget = QtGui.QWidget()

        self.grid = QtGui.QGridLayout()

        self.grid.addWidget(self.from_label,0,0)
        self.grid.addWidget(self.from_addr,1,0)
        self.grid.addWidget(self.to_label,2,0)
        self.grid.addWidget(self.to_addr,3,0)
        self.grid.addWidget(self.subject_label,4,0)
        self.grid.addWidget(self.subject,5,0)
        self.grid.addWidget(self.image,6,0)
        self.grid.addWidget(self.text,8,0)
        self.grid.addWidget(self.send,9,0)

        centralwidget.setLayout(self.grid)

        self.setCentralWidget(centralwidget)


#---------Window settings --------------------------------
        
        self.setGeometry(300,300,500,500)
        self.setWindowTitle("PyMail")
        self.setWindowIcon(QtGui.QIcon("PyMail"))
        self.setStyleSheet("font-size:15px")

    def ContextMenu(self):
        global sender
        sender = self.sender()
        
        self.menu = QtGui.QMenu(self)

        remove = QtGui.QAction("Remove",self)
        remove.triggered.connect(self.Remove)

        self.menu.addAction(remove)

        self.menu.show()

    def Remove(self):
        global sender
        global pos
        global labels
        global attachments

        pos -= 1

        ind = labels.index(sender)

        attachments.remove(attachments[ind])

        labels.remove(sender)

        sender.setParent(None)

    def Image(self):
        global path
        global attachments
        global labels
        global filetype
        global l

        path = QtGui.QFileDialog.getOpenFileName(self, "Attach file","/home/")

        if path:
            
            attachments.append(path)

            filetype = path[path.rindex(".")+1:]

            if filetype == "png":
                pic = QtGui.QPixmap(path)
            else:
                if filetype+".png" in os.listdir("C:/Python32/python/pyqt/PyMail/48px/"):
                    print("normal")
                    pic = QtGui.QPixmap("C:/Python32/python/pyqt/PyMail/48px/"+filetype+".png")
                else:
                    print("weird")
                    pic = QtGui.QPixmap("C:/Python32/python/pyqt/PyMail/48px/_blank.png")
                    
            a = QtGui.QLabel(path,self)
            a.setScaledContents(True)
            a.setFixedSize(50,50)
            a.setPixmap(pic)
            a.setToolTip(path)

            a.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
            a.customContextMenuRequested.connect(self.ContextMenu)

            labels.append(a)

            print(attachments,labels)
            
            pos = len(attachments)

            l = [self.from_label,self.from_addr,self.to_label,self.to_addr,self.subject_label,self.subject,self.image,self.text,self.send]
            
            for index,i in enumerate(l):
                self.grid.addWidget(i,index,0,1,pos+1)

                if i in l[-2:]:
                    self.grid.addWidget(i,index+1,0,1,pos+1)

            self.grid.addWidget(a,7,pos-1)
            self.setGeometry(300,300,500,550)
        
    def Send(self):
        global server
        global attachments
        global filetype
        global l
        
        fromaddr = self.from_addr.text()
        toaddr = self.to_addr.text()
        subject = self.subject.text()
        
        msg = MIMEMultipart()
        msg['From'] = fromaddr
        msg['To'] = toaddr
        msg['Subject'] = subject
        msg['Date'] = formatdate()

        body = self.text.toPlainText()
        msg.attach(MIMEText(body,"plain"))

        if attachments:
            for file in attachments:

                ctype, encoding = mimetypes.guess_type(file)

                if ctype is None or encoding is not None:
                    ctype = 'application/octet-stream'
                    
                maintype, subtype = ctype.split('/', 1)

                if maintype == 'text':
                    fp = open(file)
                    att = MIMEText(fp.read(), _subtype=subtype)
                    fp.close()
                elif maintype == 'image':
                    fp = open(file, 'rb')
                    att = MIMEImage(fp.read(), _subtype=subtype)
                    fp.close()
                elif maintype == 'audio':
                    fp = open(file, 'rb')
                    att = MIMEAudio(fp.read(), _subtype=subtype)
                    fp.close()
                else:
                    fp = open(file, 'rb')
                    att = MIMEBase(maintype, subtype)
                    att.set_payload(fp.read())
                    fp.close()
                    encoders.encode_base64(att)

                att.add_header('Content-Disposition', 'attachment', filename=file[file.rindex("/"):])
                msg.attach(att) 

        text = msg.as_string()
        
        try:
            server.sendmail(fromaddr, toaddr, text)

            msg = QtGui.QMessageBox.information(self, 'Message sent',
            "Message sent successfully, clear everything?", QtGui.QMessageBox.Yes | 
            QtGui.QMessageBox.No, QtGui.QMessageBox.Yes)

            if msg == QtGui.QMessageBox.Yes:
                self.to_addr.clear()
                self.subject.clear()
                self.text.clear()

                if attachments:
                    for i in attachments:
                        attachments.remove(i)

                    for i in reversed(range(self.grid.count())):
                        self.grid.itemAt(i).widget().setParent(None)

                    for index,i in enumerate(l):
                        self.grid.addWidget(i,index,0)
            
        except smtplib.SMTPException:
            
            msg = QtGui.QMessageBox.critical(self, 'Error',
            "The message could not be sent, retry?", QtGui.QMessageBox.Yes | 
            QtGui.QMessageBox.No, QtGui.QMessageBox.Yes)

            if msg == QtGui.QMessageBox.Yes:
                self.Send()
        
def main():
    app = QtGui.QApplication(sys.argv)
    login = Login()
    login.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

Oh yeah by the way, the file icons are from here: https://github.com/teambox/Free-file-icons

Monday, August 12, 2013

PyQt Calender App

Hi there, I recently did a sort of calender/reminder app in PyQt, using the QCalender widget. I have to say writing the tutorials like I am right now takes a lot of time, so for everything over ca. 200 lines of code I'll just post them either entirely or with comments to help you guys out a bit. Tell me what you think of this format.


#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
TheCodeInn PyQt4 Tutorial

PyQt Calender App

Author: Peter Goldsborough
Website: http://thecodeinn.blogspot.com
last edited: August 2013
'''

import sys, pickle
from PyQt4 import QtGui, QtCore

#some global variables

dateStr = "" # will hold the date
name = "" # will hold the name of an added event

gridVar = 3 # will be used to extend the grid layout

sender = "" # will hold the sender button's text
s = "" # will hold the sender button itself


e = open("events.txt","rb") #this try - except clause is there to prevent loading errors if events.txt is empty
try:
    events = pickle.loads(e.read()) 
except:
    events = {}
e.close()

class Edit(QtGui.QDialog):
    def __init__(self,parent = None):
        QtGui.QDialog.__init__(self, parent)

        #this is the dialog that opens up if a specific event is clicked
        
        self.initUI()

    def initUI(self):
        global sender

        self.change = QtGui.QLabel("Change: ",self)
        self.change.move(5,7)

        self.line = QtGui.QLineEdit(self)
        self.line.move(65,5)
        self.line.setText(sender)

        self.delt = QtGui.QPushButton("Delete",self)
        self.delt.move(80,50)

        self.ok = QtGui.QPushButton("OK",self)
        self.ok.move(40,100)

        self.cancel = QtGui.QPushButton("Cancel",self)
        self.cancel.move(120,100)
        

        self.setGeometry(300,300,240,130)
        self.setWindowTitle("Edit event")
        self.setStyleSheet("font-size:14px;")

class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.initUI()

    def initUI(self):

        self.widget = QtGui.QWidget(self)

        self.cal = QtGui.QCalendarWidget(self)
        self.cal.setGridVisible(True)
        self.cal.clicked[QtCore.QDate].connect(self.showDate)
        self.cal.setStyleSheet("font-size:15px")

        self.upc = QtGui.QLabel(self)
        self.upc.setStyleSheet("font-size:16px;")

        self.Upcoming()

        date = self.cal.selectedDate().toString()
        
        self.lbl = QtGui.QLabel(date,self)
        self.lbl.setStyleSheet("font-size:16px;")

        self.add = QtGui.QPushButton("+ Add",self)
        self.add.clicked.connect(self.AddEvent)

        self.grid = QtGui.QGridLayout()

        self.spacer = QtGui.QSpacerItem(20, 30, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)

        self.grid.addWidget(self.cal,0,0,1,2)
        self.grid.addItem(self.spacer,2,0)
        self.grid.addWidget(self.upc,1,0)
        self.grid.addWidget(self.lbl,3,0)
        self.grid.addWidget(self.add,3,1)

        self.widget.setLayout(self.grid)

#---------Window settings --------------------------------
        
        self.setGeometry(300,300,400,400)
        self.setWindowTitle("PyCal")
        self.setCentralWidget(self.widget)

#---------Slot functions --------------------------------

    def Upcoming(self):

        #this function takes care of displaying a list of upcoming events
        
        count = 0
        length = 0
        upcoming = "Upcoming events: "
        
        if events: #if events is not empty
            for i in events.values(): #for every list of events 
                for j in i: # for every event in these lists
                    length += 1
                    
            while count < 2: #set it to a higher number if you want more items displayed
                for i in events.values():
                        for j in i:
                            if length < 2:
                                upcoming += j #if there is only one event, we append it to the string and immediately exit the while loop, without this it'd display an event twice if there was only one
                                count = 2 
                            else:
                                upcoming += j+", "
                                count += 1
        else:
            upcoming += "None" #if there are no events, we display "None"

        self.upc.setText(upcoming)

    def Edit(self):

        # this function is called when an event is clicked, opening a QDialog that lets you edit the name or delete it
        
        global sender
        global s
        global events

        s = self.sender() #get the information from the sending button so we know which one it is, since they're generically created
        sender = s.text()
        
        edit = Edit(self) #modularly open the Edit QDialog
        edit.show()

        def Ok(): 

            #if the name is changed, we save the new name and delete the old one
            
            events[dateStr].remove(s.text())
            events[dateStr].append(edit.line.text())

            e = open("events.txt","wb")
            pickle.dump(events,e)
            e.close()
            
            s.setText(edit.line.text())

            self.Upcoming()

            edit.close()

        def Delete():

            #if it is deleted, we have to delete it from the layout, this is done by that for loop down there
            
            for i in reversed(range(self.grid.count())): #the reversed is there since it'd mess up the layout otherwise
                try:
                    if self.grid.itemAt(i).widget().text() == sender:
                        self.grid.itemAt(i).widget().setParent(None)
                except:
                    #you may wonder why I have a try - except clause here if I'm not doing anything when there's an exception, well, that's because the spacer
                    #is a QItem, and that would mess up the self.grid.itemAt(i).widget() up there, since the spacer is an item, not a widget
                    pass

            if len(events[dateStr]) > 1: #if there are more than one values for that day, we just remove this event
                events[dateStr].remove(sender)
            else: #if this was the day's last event we just deleted, it's good to delete the whole list, empty lists suck
                del events[dateStr]
            
            e = open("events.txt","wb")
            pickle.dump(events,e)
            e.close()

            self.Upcoming() #update the upcoming events

            edit.close()

        edit.delt.clicked.connect(Delete)
        edit.cancel.clicked.connect(lambda cancel: edit.close())
        edit.ok.clicked.connect(Ok)

    def showDate(self, date):
        global dateStr
        global events
        global gridVar

        #from here

        eventStr = ""
        gridVar = 3

        for i in reversed(range(self.grid.count())):
            try:
                self.grid.itemAt(i).widget().setParent(None)
            except:
                pass

        self.grid.addWidget(self.cal,0,0,1,2)
        self.grid.addItem(self.spacer,2,0)
        self.grid.addWidget(self.upc,1,0)
        self.grid.addWidget(self.lbl,3,0)
        self.grid.addWidget(self.add,3,1)
        
        self.setGeometry(300,300,400,400)

        #to here, we just reset the whole layout to the default - no events yet
        
        dateStr = date.toString()
        
        if dateStr in events: #now we get funky, so if the date is in the events dictionary
            for i in events[dateStr]: #we create a button for every event
                b = QtGui.QPushButton(i,self)
                b.setStyleSheet("border-radius:5px;font-size:16px;")
                b.clicked.connect(self.Edit)

                '''
                If you wonder why the buttons aren't displayed like buttons, but like labels, yet
                still work like buttons (clickable), that's because I set the border radius to 1px
                up there in the StyleSheet. QPushButton doesn't seem to support border-radius modification
                so it just displays the button flat .. neat trick if you want a clickable qlabel!
                '''
                
                self.grid.addWidget(b,gridVar,1) #extend the layout
                self.grid.addWidget(self.add,gridVar+1,1)

                gridVar +=1
                
        self.lbl.setText(dateStr)

    def AddEvent(self):
        global dateStr
        global events
        global date

        name, ok = QtGui.QInputDialog.getText(self,"Add event", #here we call an input dialog to get the event's name
                                   "Name: ")
        if dateStr not in events: #if the date isn't in the events list yet, we create a new dictionary item
            events[dateStr] = [name]
        else: #if it is, we append it to the date's events list
            events[dateStr].append(name)

        e = open("events.txt","wb")
        pickle.dump(events,e)
        e.close()

        self.showDate(self.cal.selectedDate())

        self.Upcoming()
        
def main():
    app = QtGui.QApplication(sys.argv)
    main= Main()
    main.show()

    print("I \N{black heart suit} Python")

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()



Hope you like the easter egg in my program :)

Thursday, August 8, 2013

Fully functional PyQt Calculator

This is a continuation of my tutorial on a pyqt calculator. I basically worked some more on it, adding an advanced mode. Have fun with the code:


import sys,math
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt
from math import sqrt

num = 0.0
newNum = 0.0
sumAll = 0.0
operator = ""

opVar = True

sumIt = 0

para = 0
paraVar = False
firstNum = 0
firstOp = ""

class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.initUI()

    def initUI(self):

        self.centralwidget = QtGui.QWidget(self)

        self.line = QtGui.QLineEdit(self)
        self.line.setReadOnly(True)
        self.line.setAlignment(Qt.AlignRight)
        self.line.setMinimumSize(200,25)

        zero = QtGui.QPushButton("0",self)
        zero.setMinimumSize(35,30)

        one = QtGui.QPushButton("1",self)
        one.setMinimumSize(35,30)

        two = QtGui.QPushButton("2",self)
        two.setMinimumSize(35,30)

        three = QtGui.QPushButton("3",self)
        three.setMinimumSize(35,30)

        four = QtGui.QPushButton("4",self)
        four.setMinimumSize(35,30)

        five = QtGui.QPushButton("5",self)
        five.setMinimumSize(35,30)

        six = QtGui.QPushButton("6",self)
        six.setMinimumSize(35,30)

        seven = QtGui.QPushButton("7",self)
        seven.setMinimumSize(35,30)

        eight = QtGui.QPushButton("8",self)
        eight.setMinimumSize(35,30)

        nine = QtGui.QPushButton("9",self)
        nine.setMinimumSize(35,30)

        switch = QtGui.QPushButton("+/-",self)
        switch.setMinimumSize(35,30)
        switch.clicked.connect(self.Switch)

        point = QtGui.QPushButton(".",self)
        point.setMinimumSize(35,30)
        point.clicked.connect(self.pointClicked)

        div = QtGui.QPushButton("/",self)
        div.move(130,75)
        div.setMinimumSize(35,30)

        mult = QtGui.QPushButton("*",self)
        mult.setMinimumSize(35,30)

        minus = QtGui.QPushButton("-",self)
        minus.setMinimumSize(35,30)

        plus = QtGui.QPushButton("+",self)
        plus.setMinimumSize(35,30)

        sqrt = QtGui.QPushButton("√",self)
        sqrt.setMinimumSize(35,30)

        squared = QtGui.QPushButton("x²",self)
        squared.setMinimumSize(35,30)

        equal = QtGui.QPushButton("=",self)
        equal.setMinimumSize(35,65)
        equal.clicked.connect(self.Equal)

        c = QtGui.QPushButton("C",self)
        c.setMinimumSize(70,30)
        c.clicked.connect(self.C)

        ce = QtGui.QPushButton("CE",self)
        ce.setMinimumSize(70,30)
        ce.clicked.connect(self.CE)

        back = QtGui.QPushButton("Back",self)
        back.setMinimumSize(35,30)
        back.clicked.connect(self.Back)

        self.para = QtGui.QPushButton("( )",self)
        self.para.setMinimumSize(35,30)
        self.para.clicked.connect(self.Para)
        self.para.hide()

        self.power = QtGui.QPushButton("x^y",self)
        self.power.setMinimumSize(35,30)

        self.perc = QtGui.QPushButton("%",self)
        self.perc.setMinimumSize(35,30)

        self.ln = QtGui.QPushButton("ln",self)
        self.ln.setMinimumSize(35,30)
        
        self.fact = QtGui.QPushButton("n!",self)
        self.fact.setMinimumSize(35,30)

        self.eu = QtGui.QPushButton("e",self)
        self.eu.setMinimumSize(35,30)
        self.eu.hide()

        self.pi = QtGui.QPushButton("π",self)
        self.pi.setMinimumSize(35,30)
        self.pi.hide()

        self.sin = QtGui.QPushButton("sin",self)
        self.sin.setMinimumSize(35,30)

        self.cos = QtGui.QPushButton("cos",self)
        self.cos.setMinimumSize(35,30)

        self.tan = QtGui.QPushButton("tan",self)
        self.tan.setMinimumSize(35,30)

        self.asin = QtGui.QPushButton("asin",self)
        self.asin.setMinimumSize(35,30)

        self.acos = QtGui.QPushButton("acos",self)
        self.acos.setMinimumSize(35,30)

        self.sp1 = QtGui.QPushButton(self)
        self.sp1.setMinimumSize(35,30)
        self.sp1.hide()
        self.sp1.setStyleSheet("border-radius:5px;")

        self.sp2 = QtGui.QPushButton(self)
        self.sp2.setMinimumSize(35,30)
        self.sp2.hide()
        self.sp2.setStyleSheet("border-radius:5px;")

        self.sp3 = QtGui.QPushButton(self)
        self.sp3.setMinimumSize(35,30)
        self.sp3.hide()
        self.sp3.setStyleSheet("border-radius:5px;")

        self.sp4 = QtGui.QPushButton(self)
        self.sp4.setMinimumSize(35,30)
        self.sp4.hide()
        self.sp4.setStyleSheet("border-radius:5px;")

        nums = [zero,one,two,three,four,five,six,seven,eight,nine,self.pi,self.eu]

        self.ops = [equal,self.para,back,c,ce,div,mult,minus,plus,self.power,self.perc,self.ln,self.fact,self.sin,self.cos,self.tan,self.asin,self.acos,sqrt,squared]

        for i in nums:
            i.setStyleSheet("color:blue;")
            i.clicked.connect(self.Nums)

        for i in self.ops:
            i.setStyleSheet("color:red;")

        for i in self.ops[5:10]:
            i.clicked.connect(self.Operator)

        for i in self.ops[10:]:
            i.clicked.connect(self.SpecialOperator)
            
        for i in self.ops[9:-2]:
            i.hide()

        self.grid = QtGui.QGridLayout()

#------------ Normal ------------------------

        self.grid.addWidget(self.line,0,0, 1, 5)
        self.grid.addWidget(seven,2,0, 1, 1)
        self.grid.addWidget(eight,2,1, 1, 1)
        self.grid.addWidget(nine,2,2, 1, 1)
        self.grid.addWidget(div,2,3, 1, 1)
        self.grid.addWidget(sqrt,2,4, 1, 1)
        self.grid.addWidget(four,3,0, 1, 1)
        self.grid.addWidget(five,3,1, 1, 1)
        self.grid.addWidget(six,3,2, 1, 1)
        self.grid.addWidget(mult,3,3, 1, 1)
        self.grid.addWidget(squared,3,4, 1, 1)
        self.grid.addWidget(one,4,0, 1, 1)
        self.grid.addWidget(two,4,1, 1, 1)
        self.grid.addWidget(three,4,2, 1, 1)
        self.grid.addWidget(minus,4,3, 1, 1)
        self.grid.addWidget(equal,4,4, 1, 1)
        self.grid.addWidget(zero,5,0, 1, 1)
        self.grid.addWidget(switch,5,1, 1, 1)
        self.grid.addWidget(point,5,2, 1, 1)
        self.grid.addWidget(plus,5,3, 1, 1)
        self.grid.addWidget(back,1,0, 1, 1)
        self.grid.addWidget(c,1,1, 1, 1)
        self.grid.addWidget(ce,1,3, 1, 1)

#------------ Scientific ----------------
        
        self.grid.addWidget(self.para,2,6, 1, 1)
        self.grid.addWidget(self.power,3,6, 1, 1)
        self.grid.addWidget(self.perc,4,6, 1, 1)
        self.grid.addWidget(self.ln,5,6, 1, 1)
        self.grid.addWidget(self.fact,2,7, 1, 1)
        self.grid.addWidget(self.pi,3,7, 1, 1)
        self.grid.addWidget(self.eu,4,7, 1, 1)
        self.grid.addWidget(self.sin,5,7, 1, 1)
        self.grid.addWidget(self.cos,2,8, 1, 1)
        self.grid.addWidget(self.asin,3,8, 1, 1)
        self.grid.addWidget(self.acos,4,8, 1, 1)
        self.grid.addWidget(self.tan,5,8, 1, 1)

        self.grid.addWidget(self.sp1,2,5,1,1)
        self.grid.addWidget(self.sp2,3,5,1,1)
        self.grid.addWidget(self.sp3,4,5,1,1)
        self.grid.addWidget(self.sp4,5,5,1,1)
        
        self.centralwidget.setLayout(self.grid)
        
            
#---------Window settings --------------------------------
        
        self.setGeometry(300,300,210,220)
        self.setFixedSize(212,240)
        self.setWindowTitle("PyCalc")
        self.show()

        self.setCentralWidget(self.centralwidget)

#----------- Menubar ----------------------------------

        self.menubar = self.menuBar()
        menu = self.menubar.addMenu("View")

        normal = QtGui.QAction("Normal",self)
        scientific = QtGui.QAction("Scientific",self)

        menu.addAction(normal)
        menu.addAction(scientific)

        normal.triggered.connect(self.Normal)
        scientific.triggered.connect(self.Scientific)

    def Nums(self):
        global opVar
        
        sender = self.sender()
        
        newNum = sender.text()

        print(newNum)

        if opVar == False:
            if newNum == "e":
                self.line.setText(self.line.text() + str(math.e))
                
            elif newNum == "π":
                self.line.setText(self.line.text() + str(math.pi))
                
            else:
                self.line.setText(self.line.text() + newNum)

        else:
            if newNum == "e":
                print(math.e)
                self.line.setText(str(math.e))
                
            elif newNum == "π":
                print(math.pi)
                self.line.setText(str(math.pi))
                
            else:
                self.line.setText(newNum)
            opVar = False
            
        

    def pointClicked(self):
        
        if "." not in self.line.text():
            self.line.setText(self.line.text() + ".")
            

    def Switch(self):
        global num
        
        try:
            num = int(self.line.text())
            
        except:
            num = float(self.line.text())
     
        num = num - num * 2

        numStr = str(num)
        
        self.line.setText(numStr)

    def Operator(self):
        global num
        global opVar
        global operator
        global sumIt

        sumIt += 1

        if sumIt > 1:
            self.Equal()

        num = self.line.text()

        sender = self.sender()

        operator = sender.text()
        print(operator)
        
        opVar = True

    def SpecialOperator(self):

        sender = self.sender()
        operator = sender.text()
        num = float(self.line.text())

        if operator == "ln":
            num = math.log(num)

        elif operator == "√":
            num = math.sqrt(num)

        elif operator == "x²":
            num = num ** 2

        elif operator == "n!":
            num = math.factorial(num)

        elif operator == "sin":
            num = math.sin(num)

        elif operator == "cos":
            num = math.cos(num)

        elif operator == "tan":
            num = math.tan(num)

        elif operator == "acos":
            num = math.acos(num)

        elif operator == "asin":
            num = math.asin(num)

        elif operator == "%":
            num = num / 100

        self.line.setText(str(num))

    def Equal(self):
        global num
        global newNum
        global sumAll
        global operator
        global opVar
        global sumIt
        global paraVar
        global firstNum
        global firstOp

        sumIt = 0
        if paraVar == True:
            num = firstNum
            operator = firstOp
            
        newNum = self.line.text()

        print(num)
        print(operator)
        print(newNum)
        
        if operator == "+":
            sumAll = float(num) + float(newNum)

        elif operator == "-":
            sumAll = float(num) - float(newNum)

        elif operator == "/":
            sumAll = float(num) / float(newNum)

        elif operator == "*":
            sumAll = float(num) * float(newNum)

        elif operator == "x^y":
            sumAll = math.pow(float(num),float(newNum)) 
            
        print(sumAll)
        self.line.setText(str(sumAll))  
        opVar = True
        paraVar = False

    def Back(self):
        self.line.backspace()

    def C(self):
        self.line.clear()

    def CE(self):
        global newNum
        global sumAll
        global operator
        global num
        global sumIt
        
        self.line.clear()

        num = 0.0
        newNum = 0.0
        sumAll = 0.0
        operator = ""
        sumIt = 0

    def Para(self):
        global para
        global paraVar
        global operator
        global num
        global sumAll
        global newNum
        global firstNum
        global firstOp
        global sumIt

        if para == 0:
            self.line.setText("(")

            firstNum = num
            firstOp = operator

            para = 1
            sumIt = 0
            
        else:
            self.Equal()

            paraVar = True

            para = 0

    def Normal(self):
        self.setFixedSize(212,240)

        self.grid.addWidget(self.line,0,0, 1, 5)

        self.para.hide()
        self.pi.hide()
        self.eu.hide()

        self.sp1.hide()
        self.sp2.hide()
        self.sp3.hide()
        self.sp4.hide()

        for i in self.ops[9:-2]:
            i.hide()
        
    def Scientific(self):
        self.setFixedSize(370,240)

        self.grid.addWidget(self.line,0,0, 1, 9)

        self.para.show()
        self.pi.show()
        self.eu.show()

        self.sp1.show()
        self.sp2.show()
        self.sp3.show()
        self.sp4.show()

        for i in self.ops[9:-2]:
            i.show()

def main():
    app = QtGui.QApplication(sys.argv)
    main= Main()
    main.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()


There's no memory storage, simply because I actually never really use it ... but you could add that very simply by adding a couple more buttons and global variables. The code has been tested by numerous oompa loompas in charlie's chocolate factory so everything should be fine.

Tutorial: PyQt Web Browser

Today I want to show you how to make a small, but cool, web browser using PyQt's QWebView. It will include a button for going back, forward, reloading, launching you into space, bookmarking as well as a bookmarks combo box and a status bar that displays the url to a link that is being hovered and a progress bar while the web page is loading. It will look like this:



First of all, here's the full code, only 180 lines(!) :

import sys, pickle
from PyQt4 import QtGui, QtCore
from PyQt4.QtWebKit import *

b = open("bookmarks.txt","rb")
bookmarks = pickle.loads(b.read())
b.close()

url = ""

class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.initUI()

    def initUI(self):

        global bookmarks
        
        self.centralwidget = QtGui.QWidget(self)

        self.line = QtGui.QLineEdit(self)
        self.line.setMinimumSize(1150,30)
        self.line.setStyleSheet("font-size:15px;")

        self.enter = QtGui.QPushButton(self)
        self.enter.resize(0,0)
        self.enter.clicked.connect(self.Enter)
        self.enter.setShortcut("Return")

        self.reload = QtGui.QPushButton("↻",self)
        self.reload.setMinimumSize(35,30)
        self.reload.setShortcut("F5")
        self.reload.setStyleSheet("font-size:23px;")
        self.reload.clicked.connect(self.Reload)

        self.back = QtGui.QPushButton("◀",self)
        self.back.setMinimumSize(35,30)
        self.back.setStyleSheet("font-size:23px;")
        self.back.clicked.connect(self.Back)

        self.forw = QtGui.QPushButton("▶",self)
        self.forw.setMinimumSize(35,30)
        self.forw.setStyleSheet("font-size:23px;")
        self.forw.clicked.connect(self.Forward)

        self.book = QtGui.QPushButton("☆",self)
        self.book.setMinimumSize(35,30)
        self.book.clicked.connect(self.Bookmark)
        self.book.setStyleSheet("font-size:18px;")

        self.pbar = QtGui.QProgressBar()
        self.pbar.setMaximumWidth(120)

        self.web = QWebView(loadProgress = self.pbar.setValue, loadFinished = self.pbar.hide, loadStarted = self.pbar.show, titleChanged = self.setWindowTitle)
        self.web.setMinimumSize(1360,700)

        self.list = QtGui.QComboBox(self)
        self.list.setMinimumSize(35,30)

        for i in bookmarks:
            self.list.addItem(i)

        self.list.activated[str].connect(self.handleBookmarks)
        self.list.view().setSizePolicy(QtGui.QSizePolicy.Minimum,QtGui.QSizePolicy.Minimum)

        self.web.urlChanged.connect(self.UrlChanged)
        
        self.web.page().linkHovered.connect(self.LinkHovered)

        grid = QtGui.QGridLayout()

        grid.addWidget(self.back,0,0, 1, 1)
        grid.addWidget(self.line,0,3, 1, 1)
        grid.addWidget(self.book,0,4, 1, 1)
        grid.addWidget(self.forw,0,1, 1, 1)
        grid.addWidget(self.reload,0,2, 1, 1)
        grid.addWidget(self.list,0,5, 1, 1)
        grid.addWidget(self.web, 2, 0, 1, 6)

        self.centralwidget.setLayout(grid)

#---------Window settings --------------------------------

        self.setGeometry(50,50,1360,768)
        self.setWindowTitle("PySurf")
        self.setWindowIcon(QtGui.QIcon(""))
        self.setStyleSheet("background-color:")
        
        self.status = self.statusBar()
        self.status.addPermanentWidget(self.pbar)
        self.status.hide()

        self.setCentralWidget(self.centralwidget)

    def Enter(self):
        global url
        global bookmarks
        
        url = self.line.text()

        http = "http://"
        www = "www."
        
        if www in url and http not in url:
            url = http + url
            
        elif "." not in url:
            url = "http://www.google.com/search?q="+url
            
        elif http in url and www not in url:
            url = url[:7] + www + url[7:]

        elif http and www not in url:
            url = http + www + url


        self.line.setText(url)

        self.web.load(QtCore.QUrl(url))

        if url in bookmarks:
            self.book.setText("★")
            
        else:
            self.book.setText("☆")
            
        self.status.show()
        
    def Bookmark(self):
        global url
        global bookmarks

        bookmarks.append(url)

        b = open("bookmarks.txt","wb")
        pickle.dump(bookmarks,b)
        b.close()
        
        self.list.addItem(url)
        self.book.setText("★")

def handleBookmarks(self,choice):
        global url

        url = choice
        self.line.setText(url)
        self.Enter()

def Back(self):
        self.web.back()
        
    def Forward(self):
        self.web.forward()

    def Reload(self):
        self.web.reload()

    def UrlChanged(self):
        self.line.setText(self.web.url().toString())

    def LinkHovered(self,l):
        self.status.showMessage(l)

    

def main():
    app = QtGui.QApplication(sys.argv)
    main= Main()
    main.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()


Just one thing, you need to either create a bookmark.txt file in your directory first or run the program without this: 

b = open("bookmarks.txt","rb")
bookmarks = pickle.loads(b.read())
b.close()

before having added bookmarks, otherwise it'll give you an error (since there is no bookmark.txt file yet).

Let's get started

As always, start by copying the basic PyQt window I provide at the sidebar of this blog. Then, we will need to import a few modules:

import sys, pickle
from PyQt4.QtWebKit import *

Pickle we will need for the bookmarks list, the QtWebKit for making the Bolognese sauce for our Spaghetti. Oh, wait, we're programming here. QtWebKit basically is PyQt's module for displaying web pages, QWebFrame to be more specific.

About the file pickling after, do yourself a favour and create a bookmark.txt file in your directory since you'll get an error otherwise. Lastly, create a variable url and assign it to an empty string.

Next, let's create the buttons and the QWebView and add them to a grid layout:

        global bookmarks
        
        self.centralwidget = QtGui.QWidget(self)

        self.line = QtGui.QLineEdit(self)
        self.line.setMinimumSize(1150,30)
        self.line.setStyleSheet("font-size:15px;")

        self.enter = QtGui.QPushButton(self)
        self.enter.resize(0,0)
        self.enter.clicked.connect(self.Enter)
        self.enter.setShortcut("Return")

        self.reload = QtGui.QPushButton("↻",self)
        self.reload.setMinimumSize(35,30)
        self.reload.setShortcut("F5")
        self.reload.setStyleSheet("font-size:23px;")
        self.reload.clicked.connect(self.Reload)

        self.back = QtGui.QPushButton("◀",self)
        self.back.setMinimumSize(35,30)
        self.back.setStyleSheet("font-size:23px;")
        self.back.clicked.connect(self.Back)

        self.forw = QtGui.QPushButton("▶",self)
        self.forw.setMinimumSize(35,30)
        self.forw.setStyleSheet("font-size:23px;")
        self.forw.clicked.connect(self.Forward)

        self.book = QtGui.QPushButton("☆",self)
        self.book.setMinimumSize(35,30)
        self.book.clicked.connect(self.Bookmark)
        self.book.setStyleSheet("font-size:18px;")

        self.pbar = QtGui.QProgressBar()
        self.pbar.setMaximumWidth(120)

        self.web = QWebView(loadProgress = self.pbar.setValue, loadFinished = self.pbar.hide, loadStarted = self.pbar.show, titleChanged = self.setWindowTitle)
        self.web.setMinimumSize(1360,700)

        self.list = QtGui.QComboBox(self)
        self.list.setMinimumSize(35,30)

        for i in bookmarks:
            self.list.addItem(i)

        self.list.activated[str].connect(self.handleBookmarks)
        self.list.view().setSizePolicy(QtGui.QSizePolicy.Minimum,QtGui.QSizePolicy.Minimum)

        self.web.urlChanged.connect(self.UrlChanged)
        
        self.web.page().linkHovered.connect(self.LinkHovered)

        grid = QtGui.QGridLayout()

        grid.addWidget(self.back,0,0, 1, 1)
        grid.addWidget(self.line,0,3, 1, 1)
        grid.addWidget(self.book,0,4, 1, 1)
        grid.addWidget(self.forw,0,1, 1, 1)
        grid.addWidget(self.reload,0,2, 1, 1)
        grid.addWidget(self.list,0,5, 1, 1)
        grid.addWidget(self.web, 2, 0, 1, 6)

        self.centralwidget.setLayout(grid)

Firstly, import the global variable bookmarks that we created before, which holds the (now non-existent) bookmarks. Because grid layouts only work on QWidget and not on QMainWindow, we simply create self.centralwidget and do everything on there. Then, we need to create the buttons. Most settings are self-explanatory, I simply used unicode symbols as icons (neat trick). Some aren't:

self.enter = QtGui.QPushButton(self)
self.enter.resize(0,0)
self.enter.clicked.connect(self.Enter)
self.enter.setShortcut("Return")

If you tried out the code, you will have noticed that there is no "GO" button, to enter the link. I sort of modeled this browser on google chrome, which also doesn't have one, since it's much easier to just hit enter. Since we can't set a shortcut to the QLineEdit, we have to create a sort of imaginary button, which is like a GO button, just that it's resized to 0 x 0 pixels, and therefore is not visible. Connect this button to the function self.Enter, which is this program's main funtion, and set the Shortcut to the Return button. Problem solved :-)

self.pbar = QtGui.QProgressBar()
self.pbar.setMaximumWidth(120)

Create a progress bar and set it's maximum width to 120.


self.web = QWebView(loadProgress = self.pbar.setValue, loadFinished = self.pbar.hide, loadStarted = self.pbar.show, titleChanged = self.setWindowTitle)
self.web.setMinimumSize(1360,700)

Create the QWebView and assign some variables in it's arguments (didn't know this was possible actually). These variables hold the values of the web page's loading progress, which we will need later on for the progress bar.

self.list = QtGui.QComboBox(self)
 self.list.setMinimumSize(35,30)

for i in bookmarks:
        self.list.addItem(i)

self.list.activated[str].connect(self.handleBookmarks)
self.list.view().setSizePolicy(QtGui.QSizePolicy.Minimum,QtGui.QSizePolicy.Minimum)

Here we create a combo box, which will store the bookmarks. Once bookmarks.txt will actually contain some bookmarks, they will individually be added to the combo box. Then, we need to connect the combo box to a function that handles the user clicking on one of the bookmarks, so connect it to the function self.handleBookmarks, which we will take care of later. Lastly, we need to adjust the combo box's drop down menu, since we want it to display the full links while the combo box button itself should stay at it's original size. This is done by acessing the combo box's drop down menu with .view() and changing it's size policy a bit. I actually have no clue why the size policy needs to be in there twice but I just tried and it worked whereas it wouldn't with only one policy, so yey.

  Before adding all the buttons to the layout, we need to connect two of QWebView's signals to function slots, which we will create later:

self.web.urlChanged.connect(self.UrlChanged)
        
self.web.page().linkHovered.connect(self.LinkHovered)

The first is activated when the url is changed, the second when any link is hovered.

Lastly, create a grid layout and add all the button's to it. Then make it self.centralwidget's AND NOT self's, so the main window's, layout.

Before making all the functions, create a statusbar, add the progressbar to it, and hide it for now. Also set self.centralwidget to the main windows's central widget:

self.status = self.statusBar()
self.status.addPermanentWidget(self.pbar)
self.status.hide()

self.setCentralWidget(self.centralwidget)

THE FUNCTIONS (MUAHAHA)

Now to the functions, which just sounds sort of evil, you know:




Anyway, the functions:

def Enter(self):
        global url
        global bookmarks
        
        url = self.line.text()

        http = "http://"
        
        if http not in url:
            url = http + url

        #You can delete this if you want

        elif "." not in url:
            url = "http://www.google.com/search?q="+url

        self.web.load(QtCore.QUrl(url))

        if url in bookmarks:
            self.book.setText("★")
            
        else:
            self.book.setText("☆")
            
        self.status.show()


The Enter function, which we assigned to self.enter before, takes care of opening the webpage. We call global url and bookmarks since we will need the values stored in them. Next, we fetch the url from self.line using self.line.text. The variable http is just for convenience. What follows, is an if clause, taking care of the different types of links the user inputs. Since nobody really ever types "http://" when typing a url in their webbrowser, we need to add that manually, as QWebKit needs a valid link. The other condition is my attempt at doing a direct search without having to go into google, but it's not a good one, since sometimes queries do involve dots ("F.Scott Fitzgerald") and not everybody uses Google, so delete it if you want.
Then, we load the website using self.web's load function. Also, it looks nice if you see whether or not a webpage is in your bookmarks, so we use the black unicode star instead of the white one for the bookmarking icon if the url is in the bookmarks list. Lastly, we show the statusbar.


def Bookmark(self):
        global url
        global bookmarks

        bookmarks.append(url)

        b = open("bookmarks.txt","wb")
        pickle.dump(bookmarks,b)
        b.close()
        
        self.list.addItem(url)
        self.book.setText("★")

 def handleBookmarks(self,choice):
        global url

        url = choice
        self.line.setText(url)
        self.Enter()

The second function is the one involving bookmarking. First, call the global variables. Next, we append the current url to the bookmarks list, dump it into our bookmarks.txt file, and add it to the combo box. Lastly, we set the icon star to the black one again.

HandleBookmarks is the function that is activated once a link in our bookmarks combo box is activated. This link is stored in the variable choice, which handleBookmarks needs as a parameter. So set the global variable url to this choice, show it in self.line and then just call the Enter function again.

 The following three functions don't really need to be explained, they simply do what they're named after.

The last two are overloaded functions, which we connected earlier. LinkHovered takes the parameter l (which holds the currently hovered url) and is activated whenever a link is hovered. We display this url in the status bar.

That's it! You could actually use this browser as an incognito, or private, browser, since we have no button for displaying the browsing history (you can make one). Private browsers are usually used for browsing privates. Cheers!