Having created a shell of our app in step 2, porting the image cleaner code in step 3, we’re now at the stage where we can to port our command line XML cleaner code to Qt. At the end of this post we’ll have a fully functioning C++ Qt app.
The Original ‘Command Line’ Code
-
// xml files
-
std::cout << "Starting file clean…\n";
-
-
string fileData;
-
-
d = opendir(storyPath.c_str());
-
-
if (d) {
-
-
// low memory, no parallelization
-
while ((dir = readdir(d)) != NULL) {
-
name = dir->d_name;
-
location = storyPath + "/" + name;
-
-
if (name != "." && name != ".." && name != ".DS_Store") {
-
-
// ==> buffer
-
-
// alternative version of buffer – we lose the
-
// BOM issue, but have to create \r\n
-
string fileData2;
-
string buff;
-
ifstream file;
-
file.open(location.c_str());
-
-
while (getline(file, fileData2)) {
-
buff += fileData2 + "\r\n";
-
}
-
fileData = buff;
-
-
// == remove extra headline data
-
int hl1Start = fileData.find("");
-
int hl1End = fileData.find("");
-
-
if (hl1Start != -1 && hl1End != -1) {
-
-
replaceString = fileData.substr(hl1Start,
-
(hl1End - hl1Start));
-
-
p = replaceString.find(br);
-
while (p != -1) {
-
replaceString.erase(p, 5);
-
p = replaceString.find(br);
-
}
-
-
p = replaceString.find(rl);
-
while (p != -1) {
-
replaceString.erase(p, 2);
-
p = replaceString.find(rl);
-
}
-
-
p = replaceString.find(nl);
-
while (p != -1) {
-
replaceString.erase(p, 2);
-
p = replaceString.find(nl);
-
}
-
-
// join data
-
fileData.replace(hl1Start, (hl1End - hl1Start),
-
replaceString);
-
-
} // hl1 exists
-
-
// == replace all instances with open p then find
-
// the next \r\n and add a
-
-
int bodyStart = fileData.find("");
-
int bodyEnd = fileData.find("");
-
-
int innerp;
-
-
if (bodyStart != -1 && bodyEnd != -1) {
-
-
replaceString = fileData.substr(bodyStart,
-
(bodyEnd - bodyStart));
-
-
p = replaceString.find(doublebr);
-
counter = 0;
-
int matchingP = 0;
-
while (p != -1) {
-
-
replaceString.replace(p, doublebr.size(), openp);
-
matchingP = 1;
-
-
// next \r\n [Mac Uses nl, Windows rn]
-
innerp = replaceString.find(rn, p);
-
-
if (innerp != -1) {
-
replaceString.replace(innerp, rn.size(), closep);
-
matchingP = 0;
-
}
-
-
p = replaceString.find(doublebr, p);
-
-
if (matchingP == 1) {
-
#if DEBUG == true
-
cout << "Hanging p" << endl;
-
#endif
-
replaceString.append(closep);
-
}
-
-
counter++;
-
}
-
-
// join data
-
fileData.replace(bodyStart, (bodyEnd - bodyStart),
-
replaceString);
-
-
} // bodyEnd exists
-
-
// save file
-
#if DEBUG == true
-
ofstream outfile((location + "-debug.xml").c_str(),
-
ofstream::binary);
-
#else
-
ofstream outfile((location).c_str(), ofstream::binary);
-
#endif
-
-
outfile.write(fileData.c_str(), fileData.size());
-
-
}
-
-
} // while()
-
-
}
We perform a couple of distinct steps in this code. First, we open our directory for iteration with:
-
while ((dir = readdir(d)) != NULL) {
-
name = dir->d_name;
-
[…]
And then write the contents of the file into a buffer with:
-
// BOM issue, but have to create \r\n
-
string fileData2;
-
string buff;
-
ifstream file;
-
file.open(location.c_str());
-
-
while (getline(file, fileData2)) {
-
buff += fileData2 + "\r\n";
-
}
-
fileData = buff;
Not too complicated: We use getline() to only get the raw text of a line, to which we then add a \r\n. We do this because part of our task is to recreate proper tags, and it just so happens that so long as we know where our line breaks are, we can do so reliably. Adding our own line break tags means cross-platform differences will be eliminated, plus our p tag code will run exactly as intended.
The remainder of the code removes extra formatting from headline elements, as well as adds p tags in proper order and sequence. Most of this logic, as it turns out, was the same in Qt, though we did have some significant differences in key locations.
The New Code
The new code segment is the result of a fair bit of experimentation. The command line version was in fact easier to create, though only because of the higher availability of resources for performing file opening, string manipulation, and file writing in plain old C++.
-
QString Worker::cleanXMLStoryFiles()
-
{
-
this->statusMessage = "Starting XML File Clean…";
-
-
// locals
-
QString replaceString;
-
QString fileData;
-
-
// create buffer for semaphores
-
uint count = QDir(this->xmlPath).count();
-
QString t = QString::number(count);
-
-
// loop and clean
-
QDirIterator it(this->xmlPath, QDirIterator::Subdirectories);
-
-
do{
-
-
// reset on every turn
-
fileData = "";
-
-
// open file data
-
QFile f(it.filePath());
-
if(f.open(QIODevice::ReadWrite)){
-
-
QTextStream in(&f);
-
-
do{
-
fileData += in.readLine() += "\r\n";
-
} while(!in.atEnd());
-
-
}
-
-
// remove extra headline data
-
int hl1Start = fileData.indexOf("");
-
int hl1End = fileData.indexOf("");
-
-
if(hl1Start != -1 && hl1End != -1){
-
-
replaceString = fileData.mid(hl1Start, (hl1End - hl1Start));
-
-
replaceString = replaceString.replace(this->br, "");
-
replaceString = replaceString.replace(this->rl, "");
-
replaceString = replaceString.replace(this->nl, "");
-
-
// join data
-
fileData.replace(hl1Start, (hl1End - hl1Start), replaceString);
-
-
} // hl1
-
-
// remove p tags and extra spaces
-
int bodyStart = fileData.indexOf("");
-
int bodyEnd = fileData.indexOf("");
-
-
int innerp;
-
int counter = 0;
-
int matchingP = 0;
-
int p;
-
-
if(bodyStart != -1 && bodyEnd != -1){
-
-
replaceString = fileData.mid(bodyStart, (bodyEnd - bodyStart));
-
-
p = replaceString.indexOf(this->doublebr);
-
counter = 0;
-
matchingP = 0;
-
-
while(p != -1){
-
-
replaceString.replace(p, doublebr.size(), this->openp);
-
matchingP = 1;
-
-
// next \r\n [Mac Uses nl, Windows rn]
-
innerp = replaceString.indexOf(rn, p);
-
-
if(innerp != -1){
-
replaceString.replace(innerp, rn.size(), closep);
-
matchingP = 0;
-
}
-
-
p = replaceString.indexOf(doublebr, p);
-
-
if(matchingP == 1){
-
#if DEBUG == true
-
this->statusMessage += "\nHanging P Found And Fixed\n";
-
#endif
-
replaceString.append(closep);
-
}
-
-
counter++;
-
}
-
-
// join data
-
fileData.replace(bodyStart, (bodyEnd - bodyStart), replaceString);
-
-
} // innner p
-
-
// write file
-
if(it.fileName() != "."
-
&& it.fileName() != ".."
-
&& it.fileName() != ".DS_Store"){
-
#if DEBUG == true
-
QString outPath = it.filePath() += "-new.xml";
-
#else
-
QString outPath = it.filePath();
-
#endif
-
QFile outData(outPath);
-
outData.open(QFile::WriteOnly);
-
QTextStream outFile(&outData);
-
outFile << fileData;
-
}
-
-
} while(it.next() != "");
-
-
this->statusMessage += "\nXML File Clean Complete.";
-
-
return this->statusMessage;
-
}
Some key points are as follows:
The method used to iterate through a directory:
-
QDirIterator it(this->xmlPath, QDirIterator::Subdirectories);
-
do{
-
[…]
-
} while(it.next() != "");
…feels a bit strange to me, almost as if I’m missing something. Specifically, the while() statement checks for an empty string if no further files are found. This just seems…wrong to me, but I could be mistaken. For the record, examples given in Qt help documentation had a flaw of never returning the last element of a directory. For example, if we had four items, only three would be iterated through. The do { } while() was the only way I found to hit all files.
As similar as the core logic is, here’s where things take a pretty radical departure from the command line version.
-
// reset on every turn
-
fileData = "";
-
-
// open file data
-
QFile f(it.filePath());
-
if(f.open(QIODevice::ReadWrite)){
-
-
QTextStream in(&f);
-
-
do{
-
fileData += in.readLine() += "\r\n";
-
} while(!in.atEnd());
-
-
}
In this code we’re using the handy QTextStream on an address of a QFile object to read each line of text, to which we add the \r\n to, just like the command line version.
We also save a chunk of time here by using REGEX enabled replace method to remove unwanted tags from the headline text:
-
replaceString = replaceString.replace(this->br, "");
-
replaceString = replaceString.replace(this->rl, "");
-
replaceString = replaceString.replace(this->nl, "");
As you may recall in the old code we used a more complex:
-
p = replaceString.find(rl);
-
while (p != -1) {
-
replaceString.erase(p, 2);
-
p = replaceString.find(rl);
-
}
The last part, the section where we add our properly closed p tags, is largely untouched, save for using indexOf() instead of find(), as well as using mid() to build our temporary string for editing.
The last step was to write the fixed file back to the file system. In the first version we used the rather succinct ofstream to do the job, The new version used a more complex series of command as:
-
#if DEBUG == true
-
QString outPath = it.filePath() += "-new.xml";
-
#else
-
QString outPath = it.filePath();
-
#endif
-
QFile outData(outPath);
-
outData.open(QFile::WriteOnly);
-
QTextStream outFile(&outData);
-
outFile << fileData;
Of course we may have a more simplistic way of doing this, but I didn’t find one.
Closing Thoughts
The end result of our project is a slick little Qt desktop app that removed the burden of command line operation for its use. However, as nice as this may be for the end-user, as a developer what I was most curious to see was if Qt would stand up to the challenge of being user friendly, powerful, and at the same time, a joy to work with.
Simple answer: It was. Qt is simply brilliant–I’m sold, hooked, and thoroughly excited to learn and do more with this library. It’s all the power and functionality of C++, but wrapped into a cross platform, easy to use deployment platform with powerful tools and solid documentation.
Though of course this is somewhat relative when it comes to porting command line app to a GUI. Our command line app weighed in at 223 lines, Qt: 368. While it was very easy to find examples of file operation for vanilla C++ on the web, it was markedly more difficult for Qt. In fact, as hinted at in the text I’m still quite unsure of the “properness” of this code. It works well enough, but am I missing something?
Do I really need to used a QTextStream to write data, or should I have used the more low-level write() function inherited from QIODevice? This question reminds me of the difference in PHP between using:
-
$content = "Sample";
-
$res = file_put_contents($content);
verses using:
-
$content = "Sample";
-
$handle = fopen("somefile.txt", "wb");
-
$res = fwrite($handle, $content, strlen($content));
In short, one simple step vs. a few more obtuse ones. It’s by no means wrong, but I can’t help but wonder…
Of course it should also be said that this program could use more error and sanity checks if it was to ever be released an a commercial app, but in this case, it’s just for pedagogical purposes.
Complete Application Source Download
Being a Qt app you should be able to run this code (build) on any platform Qt supports.
Download cleaner.zip