2010年4月29日星期四

OpenOffice的小工具:字數顯示器


(本來想發在另一個部落格,但想想這邊應該較多有需要的用戶,橫豎跟寫作相關,貼這邊了)
在譚劍兄的部落格看到他感嘆OpenOffice沒有像MS Word長期顯示字數的功能,在下作為OOo的支持者,便去找找看有沒有plugin可用(OOo是開放源碼軟件,吸引了不少開發者為他們編寫擴充插件)。說來有趣,我從來沒想過讓文字處理器不斷更新字數,但細心一想,這也的確很方便。找了好些插件也沒有完全合用的,於是往巨集的方向出發,結果找到一個好東西:Real-Time Word Count MACRO。

然而,這個巨集是外國人用的,所以是Word Count(字數)不是Character Count(字元數),這對咱們中文系統的沒啥用。既然如此,我便發揮典型的developer精神--它沒有這個功能,便自己寫囉。(爆)

其實也不是真的自己寫,上面那個WordCount MACRO已十分完整,只要把計算字數的地方換成計算字元便行。可是,我從沒寫過Python,到OOo看API Reference,他們又寫得含糊。我實在懶得花時間去慢慢學Python和OOo API入門,於是用囫圇吞棗的方式,利用吃完晚飯的休息時間把WordCount改成以下的CharCount:


# Continuously updating word count
import unohelper, uno, os, time
from com.sun.star.i18n.WordType import WORD_COUNT
from com.sun.star.i18n import Boundary
from com.sun.star.lang import Locale
from com.sun.star.awt import XTopWindowListener

#socket = True
socket = False
localContext = uno.getComponentContext()

if socket:
resolver = localContext.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', localContext)
ctx = resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
else: ctx = localContext

smgr = ctx.ServiceManager
desktop = smgr.createInstanceWithContext('com.sun.star.frame.Desktop', ctx)

waittime = 5 # seconds
goal = 0

def printOut(txt):
if socket: print txt
else:
model = desktop.getCurrentComponent()
text = model.Text
cursor = text.createTextCursorByRange(text.getEnd())
text.insertString(cursor, txt + '\r', 0)

def updateCount(wordCountModel, percentModel):
'''Updates the GUI.

Updates the word count and the percentage completed in the
Tkinter GUI. If some text of more than one word is selected,
it updates the GUI based on the selection; if not, on the
whole document.'''

model = desktop.getCurrentComponent()
try:
if not model.supportsService('com.sun.star.text.TextDocument'):
return
except AttributeError: return

try: wc = model.CharacterCount
except AttributeError: return

wordCountModel.Label = str(wc)

if goal != 0:
pc_text = '(%.2f %%)' % (100 * (wc / float(goal)))
percentModel.Label = pc_text

# This is the user interface bit. It looks more or less like this:

###############################
# Word Count _ o x #
###############################
# _____ #
# 451 of |500| (90.20 percent)#
# ----- #
###############################

# The boxed `500' is the text entry box.

class WindowClosingListener(unohelper.Base, XTopWindowListener):
def __init__(self):
global keepGoing

keepGoing = True
def windowClosing(self, e):
global keepGoing

keepGoing = False
e.Source.setVisible(False)

def addControl(controlType, dlgModel, x, y, width, height, label, name = None):
control = dlgModel.createInstance(controlType)
control.PositionX = x
control.PositionY = y
control.Width = width
control.Height = height
if controlType == 'com.sun.star.awt.UnoControlFixedTextModel':
control.Label = label
elif controlType == 'com.sun.star.awt.UnoControlEditModel':
control.Text = label

if name:
control.Name = name
dlgModel.insertByName(name, control)
else:
control.Name = 'unnamed'
dlgModel.insertByName('unnamed', control)

return control

def loopTheLoop(goalModel, wordCountModel, percentModel):
global goal

while keepGoing:
try: goal = int(goalModel.Text)
except: goal = 0
updateCount(wordCountModel, percentModel)
time.sleep(waittime)

if not socket:
import threading
class UpdaterThread(threading.Thread):
def __init__(self, goalModel, wordCountModel, percentModel):
threading.Thread.__init__(self)

self.goalModel = goalModel
self.wordCountModel = wordCountModel
self.percentModel = percentModel

def run(self):
loopTheLoop(self.goalModel, self.wordCountModel, self.percentModel)

def charCount(*args):
'''Displays a continuously updating character count.'''
dialogModel = smgr.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx)

dialogModel.PositionX = 300
dialogModel.PositionY = 20
dialogModel.Width = 100
dialogModel.Height = 16
dialogModel.Title = 'Char Count'

lblWc = addControl('com.sun.star.awt.UnoControlFixedTextModel', dialogModel, 2, 2, 20, 14, '', 'lblWc')
lblWc.Align = 2 # Align right
addControl('com.sun.star.awt.UnoControlFixedTextModel', dialogModel, 23, 2, 10, 14, '/')
addControl('com.sun.star.awt.UnoControlEditModel', dialogModel, 30, 2, 25, 14, '', 'txtGoal')

addControl('com.sun.star.awt.UnoControlFixedTextModel', dialogModel, 62, 2, 50, 14, '(%)', 'lblPercent')
addControl('com.sun.star.awt.UnoControlFixedTextModel', dialogModel, 114, 2, 12, 14, '', 'lblMinus')

controlContainer = smgr.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx)
controlContainer.setModel(dialogModel)

controlContainer.addTopWindowListener(WindowClosingListener())
controlContainer.setVisible(True)
goalModel = controlContainer.getControl('txtGoal').getModel()
wordCountModel = controlContainer.getControl('lblWc').getModel()
percentModel = controlContainer.getControl('lblPercent').getModel()

if socket:
loopTheLoop(goalModel, wordCountModel, percentModel)
else:
uthread = UpdaterThread(goalModel, wordCountModel, percentModel)
uthread.start()

keepGoing = True
if socket:
charCount()
else:
g_exportedScripts = charCount,

# Disclaimer and license from acb's macros
#
# Standard disclaimer and MIT licence .
# These macros are copyright (c) 2003-4 Andrew Brown.
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the 'Software'), to deal in the Software without
# restriction, including without limitation the rights to use, copy,
# modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
用法是先把以上的程式碼貼到記事本,把檔案命名為CharCount.py,放到OpenOffice下的子目錄「\Basis\share\Scripts\python」。打開OpenOffice Write,「工具→巨集→執行巨集」,在左邊選「OpenOffice.org巨集」,會找到一個叫「charcount」的項目,執行。


看,字元數目的視窗出來了。你可以在那個空格輸入目標字數,後方便顯示進度百分比(不過那個目標字數要每次輸入,暫時沒法儲存下來)。如果覺得每次打開OOo也要按一次「工具→巨集→……」很麻煩,你可以在「工具→自訂」裡設Hotkey,在「鍵盤」頁裡選一個你喜歡的熱鍵(我選Shift+F3),在下方函數種類選「OpenOffice巨集→share→charcount」,然後按上方的「修改」,以後只要按指定的鍵(例如Shift+F3)便會執行了。


放在上方,還有空位放iTune Mini-player!

原來的word count的功能更完善,可以讓用戶選擇文字,顯示選擇中的段落的字數,但我Trace Code後發覺改這部分很麻煩,OOo的API裡要計算某段文字的字元數沒有直接用的函式,自己計算的話又不準(會錯誤計算換行的字元),要準的話又花時間(運算時間),結果不改了,讓這個小程式更簡潔吧(老實說,修改期間花了80%以上的時間在研究這方面,如果單純要改成現在的樣子,真是五分鐘也不用……)。現在更新速度是五秒一次,想快些的話可以把「waittime = 5 # seconds」那個5改成3或1,不過別忘了愈小愈吃資源。

最後提醒一下,Python是一種以空白位元來計算流程的高階語言,所以不要多手把程式碼前方的空白增減耶。

台灣推理作家協會第八屆徵文獎作品集販售資訊



徵文獎作品集即將販售,這次作品集由協會自行編輯出版,將於年會會場上及網上發售。數量有限,為免向隅請從速選購。

收錄作品:
文善 〈畢業生大逃殺〉
東默農〈刑〉
何敬堯〈盡頭之濱〉
風神 〈少女的祈禱〉
高普 〈索菲亞.血色謎團〉

官方網頁:
台灣推理作家協會第八屆徵文獎作品集販售資訊
網上販售:
台灣推理作家協會第八屆徵文獎作品集(露天拍賣網站)

2010年4月20日星期二

【活動】第九屆台灣推理作家協會年會暨第八屆徵文獎頒獎典禮



年會暨頒獎禮的時間地點終於公開了!
日期:二○一○年五月二十九日(星期六)
時間:下午兩點三十分至四點三十分
地點:中國文化大學推廣教育部APA藝文中心建國本部九樓904頂級教室
(台北市建國南路二段231號,地處建國南北高架橋下,鄰近大安森林公園及台北市立圖書館,交通資訊請看這兒)

檢視較大的地圖

至於為什麼海報的調子如此詭異,是因為據說年會上將公開處刑(?),五名協會成員會被拖出來鞭打(詳情請參閱受害者冷言的網誌)(爆),對酷刑推理創作有興趣的朋友萬勿錯過啊。

官方網站:
第九屆台灣推理作家協會年會暨第八屆徵文獎頒獎典禮訊息

2010年4月10日星期六

第八屆徵文獎心得結語

六十五篇作品的心得全貼完了。除了一篇之外,所有感想也是甫讀完便寫的,其實在決選結果公布前(甚至是提交評分前)已全寫好了,只是我不想把總共四萬多字的意見一口氣貼出來,才一天貼一至兩篇,發文時改改錯別字,把一些有可能爆雷的詞語修一修等等。有一篇是在決選名單公布後才追加段落,蠻明顯的(笑),這兒不贅。我在這兒再申明一次,這些感想文章只是個人心得,看法不一定和其他評審或讀者相同,如果有作者不認同在下的意見、或者覺得我的看法有違您自己的創作理念,請不要特別在意,因為您們真正要面對的不是在下一個人,而是廣大的讀者(和編輯--如果您們投稿的話)。我不敢說自己的看法客觀,但我相信至少具有透明度,把覺得優劣的理由寫出來。

我無法預測哪篇作品能得到首獎。決選名單中的五篇作品,我最欣賞的是《索菲亞‧血色謎團》(這可以從我的感想文中輕易看出)。那個背景和題材我也一直很想寫,但自問難以寫得好,所以看到有人把兩者合二為一,令我感到相當震撼。然而這篇作品的優點或許也是缺點--當中某些趣味非凡、天馬行空的點子,可能會讓追求邏輯結構嚴謹的推理迷卻步。其次的是《少女的祈禱》,結構和謎團完整之餘,我相當喜愛當中的感情流露,可是當中犯人的心理和行為會不會被指為不夠寫實,我實在不敢說。在《盡頭之濱》和《畢業生大逃殺》的感想文中,我也挑過一些瑕疵,但相對地來說,這兩篇作品的優點也是相當出眾和獨有的,前者的拼湊式故事和描寫很是吸引,即使我現在正在寫這篇文章時,腦海中仍浮現了那個我沒見過的海濱和站在沙灘上的角色的影子;而《畢》的結局令我深感現實的無奈和殘酷,最神奇的是這篇作品壓根兒沒有犯罪事件也能帶出這樣的訊息!無可否認我不太看好《刑》,但正如我之前在心得所說,配合最近沸沸揚揚的廢死話題,這故事的切入點相當有力,可說是在推理之上帶出了更深入更貼身的意義,這一點也是十分值得重視的。

能讀到這些作品(包括落選的作品),令我體會到不少創作者具有熱誠和想法,以不同的風格去演繹他們心目中理想的推理故事。雖然在下是上屆的首獎得主,但我不覺得我比各位作者強,我只是比您們早一步踏進這個圈圈而已,期望各位和我們一起擴大這個圈圈,讓更多讀者領略閱讀推理的樂趣。

【徵文獎心得】65.失落的密碼

一九六二年,英國皇室收到恐嚇信函,女王通知軍情五處接手處理。不久,倫敦發生殘忍的凶殺案,碰巧被年輕的子爵洛克菲勒兄妹遇上,曾偵破凶案的他們便插手這一連串的事件。

作者的文筆通順,節奏掌握得很好,善用新聞報導和交替場景來作多方描述,令人讀得很舒服。故事中的線索、引用某些資料的推論亦處理得不俗,一方面顯出作者下過工夫做資料搜集,另一方面加強了故事的背景的說服力。中段的轉折有一定的意外性,可是相比之下,結局凶手的動機和出場未免弱了點。事實上,單以推理橋段來說,有好些訊息沒有在前段透露(例如死者的身分、偵探們調查到的一般資料),只在得出結論時才告訴讀者,這樣讀者的投入程度便大減。作者刻意安排的推理線是很清晰的(例如凶手寄來的金幣、死者身旁的物件),可是正正因為這些項目十分詳細,更顯得一般人會詢問的資訊的缺乏。

雖然推理方面有所缺失,但當中亦顯出作者有能力處理好伏線和解謎,我覺得可以了。然而,這作品卻有一個地方讓我頗為失望,便是故事背景的描繪。以一九六二年的英國倫敦作為舞台,如何描述異國情調、時代背景等等便十分重要,描寫技巧大大影響讀者的觀感。當我在第一段看到「倫敦警視廳」一詞便皺眉頭了。警視廳是日本用語,日本人會說「倫敦警視廳」,但中文只會用「倫敦警察廳」、「倫敦警察局」、「倫敦警察總署」或更正式的稱呼「蘇格蘭場」。我想,這或許是作者一時手誤,小問題罷,因為往下看也看到很多合乎背景的設定,雖有瑕疵(例如警長的職銜太小了,他亦沒理由直接跑去找女王)但不礙眼。不過,當中段茉莉看到屍體,驚魂甫定之際,以下一段描述令我大吃一驚:

「嗶……嗶……嗶……」手機的提示音從泰的口袋裡傳出……

手機?!這不是一九六二年嗎?為了說服自己,我唯有把這「手機」當成通話器(我肯定英國警方在七十年代初已有手提的對講機,推前十年也不太過分),可是我再讀下去--

……他急忙看了來電顯示,有些著急地看了看四周後,目光回到茉莉身上。

「來電顯示」便怎說也說不通了。這種「時代謬誤」還有後段出現的「電視遙控器」,英國電視遙控器在一九七○年代才販售,勉強說六○年代已發明出來不是不行,但在文中提到這個只令讀者感到突兀。另一方面,作者努力營造英國的古老氣氛,特意安排馬車行走--無可否認,今天也有馬車,但一九六二年汽車已經普及,就算為了突顯主角們的貴族氣派而讓他們坐馬車,凶手「乘馬車棄屍」也實在太失實了(除非凶手的目的是要引人注意)。此外像「王室經營的醫院」、「王室插手警界」也似乎跟現實有一定距離。描寫一個自己不熟悉的環境是很考工夫的,而且這次的難度還不低(地域不同外還要考慮時代差異)。我對故事中的幾位主角蠻有好感,可是假如作者想繼續發展這個故事,也許要花更多時間和精神去做資料搜集,豐富背景,襯托出劇情的趣味。