from http://qt.csdn.net/articles.aspx?pointid=178&pointid2=7
我们将构建一个完整的应用程序,使其不必太费事便可重新发布为一个真正的开源应用程序。这个应用程序就是一个RSS阅读器,它允许用户添加自己的种子,列出该种子上的内容,然后让用户在主应用程序自带的一个浏览器窗口中阅读这些内容。
如果你已经尝试过了我们前两个Qt代码项目——创建一个ffmpeg前端和创建一个媒体播放器,而且正在寻求更多Qt方面的乐趣,那么请读下去…
更新:下载此项目的代码
RSS是一个以特定方式进行格式化的XML文本文件。它包含对网站上每段内容的简短描述。它最大的优点就是,始终随着新内容的发布而更新。使用RSS阅读器或像Firefox这样与RSS兼容的浏览器时,用户可以从网站订阅RSS种子,而且阅读器将定期检查更新,并列出所有新的内容供用户浏览。而这也正是我们的应用程序所要实现的功能。
它还将引入一些主要的Qt技术,包括处理XML数据流的手段,如何动态填充树视图小部件,以及使用WebKit小部件并将所有小部件组合为一个可动态扩展的、将自动更新为显示web页面的应用程序窗口。这使得RSS阅读器成为启动更多目标远大的项目的最佳起点,即使你马上弃用RSS处理的代码,我们为这个应用程序所构建的可扩展GUI仍然可以发挥作用。
这正是首次运行Qt Creator并创建一个新项目时,需要选择三个单独的模块在应用程序中使用的原因。在向导中点击Qt4 GUI Application模块,给它取一个名字,然后启用如下三个模块:QtNetwork, QtWebkit 和 QtXML。这些模块将紧密联系我们将在本指南中讲到的三个新领域,而且从向导添加它们后,便不用再手动把它们添加到项目的“.pro”文件中。
设计GUI
和我们其他的Qt编程指南一样,在运行Creator创建一个新项目后,接下来要做的工作就是GUI设计。点击“ui”文件打开Designer视图。这一次,我们将采用稍微开放一点的方法进行设计。主窗口将被划分为两个面板。在左侧,我们将添加RSS消息列表,并让用户能够添加他们自己的种子。而窗口的右半部分将是web浏览器,我们将对这部分使用WebKit小部件。
但是Qt的聪明之处在于,我们可以根据用户是否想使用内部浏览器来使每个面板变得可扩展或可隐藏,或者干脆使用他们最惯于使用的浏览器。例如,如果用户不想看到web视图,只需要把中间的分离线拖到右边,它就会消失。这给予了我们的应用程序很大的灵活性,并不强迫想使用自己的浏览器阅读新闻的用户使用web视图。
这项特别的功能是通过Qt中的Dock Widget小部件实现的。当应用程序分为几个部分时,它提供了很强的灵活性,允许用户在四周拖放窗口的不同部分。从Creator页面的Containers列表中拖出两个Dock Widget小部件到空白的应用程序画布上。如果在应用程序中用不着,还可以从Object视图删除多余的菜单、工具栏和状态面板小部件。我们已经添加了两个可停靠小部件,因为我们要在应用程序的两侧使用它们来保存小部件,而且它们是可停靠小部件,用户能够拖动它们之间的分离线,从而改变应用程序每个半区的尺寸。
但在添加更多小部件之前,我们需要确保只启用了每个可停靠小部件的一组有限功能。我们不想让用户完全访问Qt 可停靠小部件更多难以驾驭的功能,KDevelop已经很好地证明了这一点。在Object列表中选择每个可停靠小部件,然后在下面的属性窗口中,确保将选中的“Features”字段设置为“NoDockWidgetFeatures”。这将阻止用户将小部件拖动至窗口外部或者完全关闭它们。你可能想对浏览器面板启用这项功能,这由你自己决定。
可停靠小部件的优点是,用户可在应用程序运行时改变两侧的比例差异。
小部件面板
在添加其他小部件之前,选择可停靠小部件并点击“Lay Out Horizontally”按钮。接着点击“Lay Out in a Grid”按钮。这样做的效果是同时拉伸跨应用程序窗口的、中间具有一条分离线的两个可停靠小部件。当用户改变主窗口的大小时,这两个小部件将保持它们的相对位置。
尽管网格被锁定,我们仍然能够以常规方式给可停靠小部件添加小部件,而且我们准备从左侧开始。如果在网格被锁定的情况下编辑GUI,Designer将使用蓝色光标突出显示每个小部件要插入的位置,这一点十分类似于字处理器。需要将三个小部件拖动到左边面板中——一个行编辑小部件和一个按钮,它们已经在窗口顶部水平对齐了,还有一个位于下方的树视图。行编辑小部件用于给用户输入RSS种子的URL,按钮用于提交种子给我们的解析器,而树视图用于列出RSS种子的每个入口。
我们给行编辑小部件添加了一个默认的URL。只要双击该小部件,然后输入类似于“http://www.tuxradar.com/rss”的内容,再将按钮文本改为“Add Feed”。双击树视图,再添加两列,将它们分别取名为“Feed”、“Date”和“URL”。这些列将包含每个新闻内容的信息,但只有“Feed”和“Date”两列可见。这是因为我们要内部使用URL列,不显示给用户看。它将保存内容的URL,这样当用户点击它时,我们可以把URL发送给WebKit。
如果我们不使用这种方法,我们就不得不为应用程序实现一个成熟的MVC解决方案,而这描述起来都超过4页纸了。MVC(模型/视图/控制器)是一种将数据(在这个例子中是指URL)与显示数据的视图分离,同时保持二者联系的方法。后一部分由控制器来处理。当我们使用它的任意容器类时,Qt在后台使用的是MVC,而它用于添加和删除内容项的方法实际上是用于在后台处理MVC的便利函数。我们将在树视图中利用这一点,隐藏URL列并在应用程序中使用数据,但我们只能在源代码中做到这一点。
最后,将WebKit小部件拖动到右侧面板中。这是一个自包含的浏览器窗口,我们不需要添加任何别的内容就可以让它工作。只要保证所有小部件都经过了正确排列,以及你已经在两个面板上使用了一些间距器和“Lay Out in the Grid”模式,从而锁定可缩放窗口的布局。
在可停靠小部件的左侧,我们添加了树视图,XML 提要的 URL,以及用于从Internet抓取数据的按钮。
连接
既然我们的布局已经最终确定,下一步就要添加槽/信号连接,用于补充我们应用程序的功能。切换到Signals/Slots编辑器,方法是按下F4键或者在工具栏中点击相应按钮。从“Add Feed”按钮拖动一个信号到应用程序窗口的轮廓处,当“Configure Connection”窗口出现时,点击右侧面板上的“Edit”按钮。
我们需要添加两个槽。第一个用于给树视图添加种子,而另一个用于当用户在种子列表中选择一个新闻内容时更新web视图。我们将第一个槽称为“fetch()”,而将第二个槽称为“itemActivated(QTreeWidgetItem*)”。这是我们首次遇到通过信号/槽机制传递的参数,要在设计器中使用它们,必须满足一些严格的规则。其中最重要的一条是,对于一个在传递这类参数时要连接到槽的信号,二者都必须完全支持同一类型。在这个例子中是QTreeWidgetItem类型。
创建这两个槽并将“clicked”连接到“fetch()”之后,从树视图拖一个新连接到窗口背景。我们将看到,很多函数将QItemTreeTree参数作为一个参数包含在内。这是树视图中每一项的类型,以这种方式传递它使我们能够轻松抓取到当前选中的新闻内容的URL,并使用它来更新web浏览器。只要将位于左边的“/'itemActivated(QTreeWidgetItem*)”与我们刚刚为自己的应用程序创建的名称相同的新槽连接起来即可。
这是我们在Creator中建立并用于应用程序中各个函数的SIGNAL和SLOT连接的一个视图。
代码
现在我们已经建立了框架,是时候添加代码了。和我们其他的项目一样,我们从“mainwindow.h”开始,把它作为需要添加我们刚刚在GUI中创建的新槽的地方。我们还准备添加要在程序逻辑中使用的新槽,用于告诉我们的应用程序,从Internet读取web数据的过程已经结束。
void fetch();
void itemActivated(QTreeWidgetItem * item);
void readData(const QHttpResponseHeader &);
现在,我们需要给项目添加一些私有成员。我们将使用这些私有成员管理数据流,并且为解析从站点的RSS种子抓取到的XML数据和HTML数据而创建数据结构。
void parseXml();
QString currentTag;
QString linkString;
QString titleString;
QString dateString;
QTreeWidgetItem *feed;
int connectionId;
QHttp http;
QXmlStreamReader xml;
这是我们需要给头文件添加的内容。我们余下的编码将限制在“mainwindow.cpp”文件中,从位于该文件顶部的初始化函数开始。首先,我们需要在“setupUi”前面添加一个连接行,用于当我们知道Qt的HTTP抓取器已经正确解析HTTP时,自动运行我们的“readyRead”方法。其次,我们想隐藏treeWidget的两列,因为我们只使用这些列来保存数据,而不想让用户看到它们。一旦“setupUi”创建了GUI,我们就可以这样修改它。下面给出相应的代码:
connect(&http, SIGNAL(readyRead(const QHttpResponseHeader &)), this,
SLOT(readData(const QHttpResponseHeader &)));
ui->setupUi(this);
ui->treeWidget->setColumnHidden(1, true);
ui->treeWidget->setColumnHidden(2, true);
现在,我们准备编写fetch()函数。当我们在应用程序中输入RSS种子的URL,然后点击“Add Feed”按钮时,将触发这个函数的功能。
void MainWindow::fetch()
{
xml.clear();
QUrl url(ui->lineEdit->text());
http.setHost(url.host());
connectionId = http.get(url.path());
}
这段代码相对较为直观。首先,我们清理了保存XML日期的流读取对象,然后将我们用于保存URL的行编辑组件中的文本转换为一个QUrl,这是Qt中访问在线资源的首选方法。接下来,我们使用这个资源设定QHttp的位置,这个类是用于实现HTTP协议的。我们需要 使用这个类来抓取XML数据,而下一行中使用“get”函数和经过转换的URL调用了这个函数。当“http”成功打开HTTP位置时,它将发出我们前面连接到我们自己的“readData”函数的“readyRead”信号。现在我们需要添加这个函数:
void MainWindow::readData(const QHttpResponseHeader &resp)
{
if (resp.statusCode() != 200)
http.abort();
else {
xml.addData(http.readAll());
parseXml();
}
}
这个函数的全部功能就是检查是否找到了URL,如果没有找到,它会中断,而且我们的应用程序也不会再往下执行。但如果远程位置是合法的,在把数据发送给“parseXML”函数之前,我们首先会使用数据填满我们的XML容器——xml.addData(http.readAll())。这是应用程序的一个难点,因为需要遍历从internet抓取的XML树,并把我们需要的数据块放到treeView中。因此,对应代码的篇幅要长很多。
void MainWindow::parseXml()
{
while (!xml.atEnd()) {
xml.readNext();
if (xml.isStartElement()) {
if (xml.name() == "item"){
if (titleString!=""){
feed = new QTreeWidgetItem;
feed->setText(0, titleString);
feed->setText(2, linkString);
ui->treeWidget->addTopLevelItem(feed);
}
linkString.clear();
titleString.clear();
dateString.clear();
}
currentTag = xml.name().toString();
} else if (xml.isEndElement()) {
if (xml.name() == "item") {
QTreeWidgetItem *item = new QTreeWidgetItem(feed);
item->setText(0, titleString);
item->setText(1, dateString);
item->setText(2, linkString);
ui->treeWidget->addTopLevelItem(item);
titleString.clear();
linkString.clear();
dateString.clear();
}
} else if (xml.isCharacters() && !xml.isWhitespace()) {
if (currentTag == "title")
titleString += xml.text().toString();
else if (currentTag == "link")
linkString += xml.text().toString();
else if (currentTag == "pubDate")
dateString += xml.text().toString();
}
}
if (xml.error() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
qWarning() << "XML ERROR:" << xml.lineNumber() << ": " << xml.errorString();
http.abort();
}
}
这段代码看起来有点吓人,但这主要是因为它包含了几条嵌套的“if”语句,用于处理我们在RSS种子中将会遇到的不同类型的XML元素。
我们从依次读取每个元素开始,然后检查该元素在树上是属于新的内容项,一个元素的末端,还是包含真正的元素数据。
当代码检测到XML种子中一个新元素的起点时,它会将“currentTag”设为该元素中所包含数据的类型。我们只对“title”、“link”和“pubDate”字段感兴趣,而且当XML流移动到文件的字符部分时,每个字段的文本就会根据currentTag类型转移到“titleString”、“linkString”和“dateString”中。当检测到一个元素的终点时,我们已经知道这些字符串是否已经被填充,以及是否可以把数据复制到我们GUI中的“treeView”对象中。这就是“item->setText”行的作用,而这些字符串被添加到一个treeView顶部的内容项中,该treeView创建来保存“StartElement”部分中的每个RSS种子的树视图。
如果我们在理解这个函数时有困难,使用Creator的优秀调试器把代码完整地运行一遍,会对我们有所帮助。如果在这个函数中设置一个断点,当应用程序的执行到达代码的这个部分时,我们就能够使用Debug菜单单步调试每一行,并监视感兴趣参数的值。
最后,我们需要添加的最后一个函数是用于在用户点击RSS新闻项之一时,将该新闻项指向的web页面装载到我们的WebKit小部件中。这个函数是使用我们在GUI设计器中建立的“itemActivated” SIGNAL/SLOT连接来执行的。我们从树小部件内容项中找到了由信号传递的URL,并把这些数据发送给webView小部件,然后把它转换为一个QUrl,而我们也正是这样做的。幸运的是,此功能一共只需要两行代码:
void MainWindow::itemActivated(QTreeWidgetItem * item)
{
ui->webView->load(QUrl(item->text(2)));
ui->webView->show();
}
运行应用程序
以上就是需要完成的全部工作。剩下的就是保存项目,编译和运行。点击“Add Feed”按钮可以添加我们在GUI中创建的默认RSS种子,而且我们应该看到,位于左侧的树视图使用了来自TuxRadar.com的所有最新内容进行填充。点击其中任意的内容,右侧的web查看器就会加载相应的页面。
但这个应用程序的最大优点是缩放两个面板的方式。在树视图和web页面之间,应该能够找到三个很小的垂直点。可以把这些点拖到左边或右边,从而改变RSS种子或正在显示的web页面的比例。如果将这一条完全移到右边,web视图就会完全关闭。如果我们只想看到种子列表,这是个不错的主意。
可以轻松给这个应用程序添加所需要的内容。我们从一个具有自动功能的刷新按钮开始。这个按钮功能是大约每小时增加一次新种子,或者在点击它时手动刷新。应用程序还迫切需要保存其设置的功能。这是一项十分艰巨的任务,几乎可以作为另一份指南的主题——为什么不亲自写写试试看呢?