用PyQt4+Python写一个简单的EPub阅读器(2/3)

上一篇,这一篇我们写GUI。

上一篇提出了图书仓库的概念,更具体的想法是:这个仓库是一个文件夹,所有打开的书都往这个文件夹中复制一份,同时,我们在仓库中有一个library.json,存放书籍清单,每次打开一本书,也在该清单中记录一份,根据清单刷新我们的Library(dockwidget目录)

目前目录结构如下:

GUI

这里只是写GUI,所以不做过多的讲解,画GUI也真的没有什么好讲的。当然,这里画GUI用的是比较繁琐的方式,用Qt creator画出界面再用pyuic4生出py文件会比较方便一点,讲真,中文的PyQt的资料实在太少了,有空的话可以写一个中文教程(好像又给自己挖坑了)。这里我就直接贴代码了。

项目中总会有一些常量,我们把它记录在constants.py中,同时这个模块进行初始化的操作,新建必要的文件夹,数据文件。

import os

PROJECT_DIR = os.path.abspath(os.path.dirname(__file__))
LIBRARY_DIR = os.path.join(PROJECT_DIR, 'bookdata') + os.sep

if not os.path.exists(LIBRARY_DIR):
    os.mkdir(LIBRARY_DIR)

LIBRARY = os.path.join(LIBRARY_DIR, "library.json")

if not os.path.exists(LIBRARY):
    open(LIBRARY, 'w').close()

由于目录结构的变化,上一篇写的books.py也有一点变化,LIBRARY_DIR可以从constants模块中导入,还要加上两行

parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parentdir)

这样就能从父模块中导入constants,所以books.py就变成了:

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

import os
import zipfile
import sys

from lxml import etree
from BeautifulSoup import BeautifulStoneSoup
parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parentdir)

from constants import LIBRARY_DIR

# LIBRARY_DIR = os.path.abspath('.') + os.sep

RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True)
NAMESPACES = {
    'dc': 'http://purl.org/dc/elements/1.1/',
}


class Book(object):
    u"""
    需要主动调用open方法才能获得相应的属性
    """
    _FILE = LIBRARY_DIR + '%s.epub'

    def __init__(self, book_id=None):
        if book_id:
            self.open(book_id)

    def fromstring(self, raw, parser=RECOVER_PARSER):
        return etree.fromstring(raw, parser=parser)

    def read_doc_props(self, raw):
        u"""

        :param raw: raw string of xml
        :return:
        """
        root = self.fromstring(raw)
        self.title = root.xpath('//dc:title', namespaces={'dc': NAMESPACES['dc']})[0].text
        self.author = root.xpath('//dc:creator', namespaces={'dc': NAMESPACES['dc']})[0].text

    def open(self, book_id=None):
        if book_id:
            self.book_id = book_id
        if not self.book_id:
            raise Exception('Book id not set')

        self.f = zipfile.ZipFile(self._FILE % self.book_id, 'r')
        soup = BeautifulStoneSoup(self.f.read('META-INF/container.xml'))

        oebps = soup.findAll('rootfile')[0]['full-path']
        folder = oebps.rfind(os.sep)
        self.oebps_folder = '' if folder == -1 else oebps[:folder+1]   # 找到oebps的文件夹名称

        oebps_content = self.f.read(oebps)
        self.read_doc_props(oebps_content)

        opf_bs = BeautifulStoneSoup(oebps_content)
        ncx = opf_bs.findAll('item', {'id': 'ncx'})[0]
        ncx = self.oebps_folder + ncx['href']     # 找到ncx的完整路径

        ncx_bs = BeautifulStoneSoup(self.f.read(ncx))

        self.chapters = [(nav.navlabel.text, nav.content['src']) for
                         nav in ncx_bs.findAll('navmap')[0].findAll('navpoint')]

if __name__ == '__main__':
    book = Book('莎士比亚全集')
    print book.oebps_folder

    print book.title
    print book.author

    print str(book.chapters).decode("unicode-escape").encode("utf-8")

接下来,是bookview.py

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

from PyQt4.QtGui import (QWidget, QPushButton, QHBoxLayout, QVBoxLayout,
                         QListWidget, QLabel, QSplitter)
from PyQt4.QtWebKit import QWebView


class BookView(QSplitter):
    def __init__(self, parent=None):
        super(BookView, self).__init__(parent=parent)
        self.create_layout()

    def create_layout(self):
        self.web_view = QWebView()
        self.chapter_list = QListWidget()
        self.next_button = QPushButton("Next chapter")
        self.previous_button = QPushButton("Previous chapter")

        hbox = QHBoxLayout()
        hbox.addStretch()
        hbox.addWidget(self.previous_button)
        hbox.addWidget(self.next_button)

        vbox = QVBoxLayout()
        vbox.addWidget(QLabel("Chapters"))
        vbox.addWidget(self.chapter_list)
        vbox.addLayout(hbox)

        widget = QWidget()
        widget.setLayout(vbox)

        self.addWidget(self.web_view)
        self.addWidget(widget)

library.py :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import os
import sys

from PyQt4.QtGui import QTableWidget, QTableWidgetItem
from PyQt4.QtCore import Qt

parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parentdir)

from constants import LIBRARY


def get_library():
    with open(LIBRARY, 'r') as f:
        try:
            library = json.load(f)
        except Exception, e:
            print(e)
            library = {'books': []}
    return library


def insert_library(book):
    u"""

    :param book: books.py中定义的类型, 有id, 有title, 有authors
    :return:
    """
    lib = get_library()
    book.open()
    lib['books'].append({'id': book.book_id, 'title': book.title, 'author': book.author})

    with open(LIBRARY, 'w') as f:
        json.dump(lib, f, indent=4)


# 下面的GUI代码不应该跟逻辑代码写在一起,这里的写法不是好例子
class LibraryTableWidget(QTableWidget):

    def __init__(self, book_view, parent=None):
        super(LibraryTableWidget, self).__init__(parent=None)
        self.book_view = book_view

        self.setColumnCount(2)
        self.refresh()

    def refresh(self):
        self.library = get_library()

        self.clear()
        self.setRowCount(len(self.library['books']))
        self.setHorizontalHeaderLabels(['Title', 'Authors'])

        for i, book in enumerate(self.library['books']):
            for j, cell in enumerate((book['title'], book['author'])):
                item = QTableWidgetItem(cell)
                item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsEnabled)
                self.setItem(i, j, item)

        self.resizeColumnsToContents()

    def create_connections(self):
        pass

    def view_book(self):
        book_id = self.library['books'][self.currentRow()]['id']
        self.book_view.load_book(book_id)

window.py

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

import os
import sys
import shutil

from PyQt4.QtCore import Qt, SIGNAL, SLOT
from PyQt4.QtGui import (QMainWindow, QDockWidget, QAction, QApplication,
                         QMessageBox, QFileDialog)

from library import LibraryTableWidget, insert_library
from bookview import BookView
from books import Book

parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parentdir)

from constants import LIBRARY_DIR


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.create_layout()
        self.create_actions()
        self.create_menus()
        self.create_connections()

    def create_layout(self):
        self.book = BookView(self)
        self.setCentralWidget(self.book)

        self.create_library_dock()

    def create_library_dock(self):
        if getattr(self, 'dock', None):
            self.dock.show()
            return

        self.dock = QDockWidget("Library", self)
        self.dock.setAllowedAreas(Qt.LeftDockWidgetArea|Qt.RightDockWidgetArea)
        self.library = LibraryTableWidget(self.book)
        self.dock.setWidget(self.library)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock)

    def create_menus(self):
        file_menu = self.menuBar().addMenu("&File")
        help_menu = self.menuBar().addMenu("&Help")

        file_menu.addAction(self.library_action)
        file_menu.addAction(self.open_action)
        file_menu.addSeparator()
        file_menu.addAction(self.quit_action)

        help_menu.addAction(self.help_action)
        help_menu.addAction(self.about_action)

    def create_actions(self):
        self.library_action = QAction("&Library", self)
        self.open_action = QAction("&Open", self)
        self.quit_action = QAction("&Quit", self)

        self.help_action = QAction("Help", self)
        self.about_action = QAction("&About", self)


    def create_connections(self):
        self.connect(self.library_action, SIGNAL("triggered()"), self.create_library_dock)
        self.connect(self.open_action, SIGNAL("triggered()"), self.open_book)
        self.connect(self.quit_action, SIGNAL("triggered()"), QApplication.instance(),
                     SLOT("closeAllWindows"))
        self.connect(self.about_action, SIGNAL("triggered()"), self.about)
        self.connect(self.help_action, SIGNAL("triggered()"), self.help)

    def about(self):
        QMessageBox.about(self, "QtBooks", "An ebook reader")


    def help(self):
        QMessageBox.information(self, 'Help', 'Nothing yet!')

    def open_book(self):
        book_path = QFileDialog.getOpenFileName(self, u'打开Epub格式电子书', ".", "(*.epub)")

        print u"in open_book, book_name is:" + str(book_path)
        print u"in open_book, bookdata path:" + str(LIBRARY_DIR)
        print os.path.dirname(str(book_path))

        if os.path.dirname(str(book_path))+os.sep != str(LIBRARY_DIR):
            shutil.copy(str(book_path), LIBRARY_DIR)

        file_name = os.path.basename(str(book_path))
        book_id = file_name.split('.epub')[0]
        book = Book(book_id)
        insert_library(book)
        self.library.refresh()        

最后是main.py:

# -*- coding: utf-8 -*-

#!/usr/bin/env python

import sys

from PyQt4.QtGui import QApplication
from src.window import MainWindow


reload(sys)
sys.setdefaultencoding('utf8')

def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

MainWindow的部分除了GUI,还加上了几个无关紧要的弹出对话框的内容,涉及到Qt的信号槽机制,这部分留到下一篇。

只加上了一段逻辑代码,可以打开epub文件,并将该文件复制到仓库中(文件系统中), 同时刷新LibraryTableWidget的内容,使得书名,作者显示出来:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容