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是一種以空白位元來計算流程的高階語言,所以不要多手把程式碼前方的空白增減耶。

6 則留言:

  1. 待用,先多謝你一聲 :)

    回覆刪除
  2. 譚劍>
    別客氣,希望合用。 :)

    回覆刪除
  3. 用小筆電時會用到OOo,謝謝你這個「發展」了。 :)

    回覆刪除
  4. 貝爾>
    別客氣~~~ :)

    回覆刪除
  5. 想請問一下,這個字數顯示器可以放在MS OFFICE使用嗎?該怎麼做呢?多謝你喔!

    回覆刪除
  6. Gilbert>
    這個小程式只適用於OpenOffice喔。不過MS Office不是內置了顯示字數的功能嗎?(我沒有MS Office,所以不大清楚)

    回覆刪除