乐者为王

Do one thing, and do it well.

如何使文字与图标垂直居中对齐

在做网页的时候,经常会需要在某段文字前加上一个图标。然后就会发现增加的图标和文字的位置不齐,文字总是比图标低点。

smiley-smile微笑

解决的方法有两个:一个是设置图标的vertical-align为top;还有就是将margin-bottom设为-3px。

1
2
<img style="vertical-align: top" src="/uploads/smiley-smile.gif" border="0" />微笑
<img style="margin-bottom: -3px" src="/uploads/smiley-smile.gif" border="0" />微笑

实现自己的Java Annotation

Annotation类型的声明与一般的接口声明极其相似,区别只在于它在interface关键字前面使用“@”符号。下面就是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Event {
}

public class EventAnonationTest {
    @Event
    public void clicked() {
    }
}

@Target指定可以应用Annotation类型的程序元素,以防止在其它程序元素中误用Annotation类型:

1
2
3
4
5
6
7
8
9
10
public enum java.lang.annotation.ElementType {
    TYPE,               // Class, interface, or enum (but not annotation)
    FIELD,              // Field (including enumerated values)
    METHOD,             // Method (does not include constructors)
    PARAMETER,          // Method parameter
    CONSTRUCTOR,        // Constructor
    LOCAL_VARIABLE,     // Local variable or catch clause
    ANNOTATION_TYPE,    // Annotation Types (meta-annotations)
    PACKAGE             // Java package
}

@Retention设置Java编译器处理Annotation类型的方式:

1
2
3
4
5
public enum java.lang.annotation.RetentionPolicy {
    SOURCE,     // Annotation is discarded by the compiler
    CLASS,      // Annotation is stored in the class file, but ignored by the VM
    RUNTIME     // Annotation is stored in the class file and read by the VM
}

@Documented指明需要在Javadoc中包含Annotation(缺省是不包含的)。使用@Documented的一个技巧就是指定Retention为RetentionPolicy.RUNTIME。这样,Annotation就会保留在编译后的类文件中并且由虚拟机加载,然后Javadoc就可以抽取出Annotation并添加到类的HTML文档中。

@Inherited定义了Annotation类型的修饰是否可以由被修饰类的子类继承。

wxWidgets Wizard for Visual Studio 2005/2008

花了点时间为VS2005/VS2008写了一个简单的wxWidgets Wizard。本来想写的更完善一些再发布出来的,只是实在没有多少心思再继续地写下去了。而且该向导程序已经基本可以满足我自己的要求了。不过,如果以后有时间的话还是会继续完善它的。以下是我原本打算要实现的一些功能:

  1. 支持创建对话框程序;
  2. 可以直接安装wxWidgets Wizard到VS2005中的安装程序;
  3. 集成wxWidgets Help文档到VS2005中;
  4. 实现代码智能提示功能;
  5. 可以在VS2005中直接编辑和编译XRC资源;
  6. 在向导过程中可以设置一些wxWidgets的高级特性(比如Menu Bar,Status Bar等)。

截图:

wxwizard-default

wxwizard-apptype

wxwizard-generated-classes

代码下载:https://github.com/dohkoos/wxwizard

如何调试Win32程序

方法一:使用OutputDebugString函数

函数的原型如下:

1
void OutputDebugString(LPCTSTR lpOutputString);

该函数会输出信息到系统的DEBUGER,输出结果可以使用工具DebugView观察。因为OutputDebugString的参数是字符串,而我们在实际使用过程中通常希望能像printf一样支持变参。下面的方法实现了这个效果:

1
2
3
4
5
6
7
8
9
10
void DebugString(LPCTSTR lpszFormat, ...)
{
    va_list args;
    TCHAR szText[1024];

    va_start(args, lpszFormat);
    wvsprintf(szText, lpszFormat, args);
    OutputDebugString(szText);
    va_end(args);
}

方法二:输出调试信息到Console上

1
2
3
4
FILE *stream;
AllocConsole();
freopen_s(&stream, "CONOUT$", "w", stdout);
printf("Hello, world!\n");

这里AllocConsole()用来打开Console,而freopen_s则把标准输出和Cosole关联。“CONOUT$”这个很关键。

C#开发BHO插件UrlTrack

最近忽然突发奇想,想统计一下我最经常上的网站是哪些,并且在这些网站上都停留了多久。为此决定写一个BHO插件来做这件事。

BHO(Browser Help Objects)是实现了特定接口(IObjectWithSite)的COM组件。开发好的BHO插件除了要在注册表中注册为COM Server外,还必须将它的CLSID在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects下注册为子键。每当浏览器[1]启动时,首先会在上述注册表位置查看是否有注册的BHO CLSID,如果有则分别创建一个实例,并对BHO实例进行初始化。BHO实例运行在浏览器的地址空间内,能对可访问的对象(如窗口、模块等等)执行任何操作,且因为它依附于浏览器的主窗口,所以其生命周期与浏览器实例的生命周期一致。下图演示了BHO的创建过程:

bho-process

下面就来介绍一下如何开发BHO插件。首先创建一个C#项目,类型为Class Library。然后将Class1.cs改名为IObjectWithSite.cs,还要给IObjectWithSite添加两个功能:GetSite和SetSite。

1
2
3
4
5
6
7
Public Interface Iobjectwithsite
{
    [Preservesig]
    Int Setsite([Marshalas(Unmanagedtype.Iunknown)]Object Site);
    [Preservesig]
    Int Getsite(Ref Guid Guid, Out Intptr Ppvsite);
}

添加一个UrlTrack.cs文件,并且实现IObjectWithSite接口。使用BHO还需要添加两个引用SHDocVw.dll和MSHTML.dll,可以在Windows\System32目录下找到。

bho-references

在IObjectWithSite.cs中,还需要为我们的程序指出IE的GUID,使得它可以挂接(attach)到IE上:

1
2
3
4
5
[
ComVisible(true),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("FC4801A3-2BA9-11CF-A229-00AA003D7352")
]

另外,还需要给BHO程序分配一个GUID,这个可以通过System.Guid.NewGuid()方法得到。

1
2
3
4
5
[
ComVisible(true),
Guid("e90da13b-117a-4178-8111-0f712da09ff9"),
ClassInterface(ClassInterfaceType.None)
]

在UrlTrack.cs中,我们还需要写两个方法用来DLL注册和移除注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static string BHOKEYNAME = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects";

[ComRegisterFunction]
public static void RegisterBHO(Type type)
{
    RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(BHO_KEY_NAME, true);
    if (registryKey == null)
    {
        registryKey = Registry.LocalMachine.CreateSubKey(BHO_KEY_NAME);
    }

    string guid = type.GUID.ToString("B");
    RegistryKey bhoKey = registryKey.OpenSubKey(guid, true);
    if (bhoKey == null)
    {
        bhoKey = registryKey.CreateSubKey(guid);
    }
    // NoExplorer: dword = 1 prevents the BHO to be loaded by Explorer.exe
    bhoKey.SetValue("NoExplorer", 1);
    bhoKey.Close();

    registryKey.Close();
}

[ComUnregisterFunction]
public static void UnregisterBHO(Type type)
{
    RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(BHO_KEY_NAME, true);
    string guid = type.GUID.ToString("B");

    if (registryKey != null)
        registryKey.DeleteSubKey(guid, false);
}

接下来就是实现具体的统计功能了。考虑一下,当输入网址后,我们需要记录下网址以及当前的时间;当在同一浏览窗口中切换网址时,不仅需要记录下网址和当前时间,还要设置前一个浏览记录的结束时间;并且在关闭浏览器时,也要记下结束时间。所以在SetSite中需要挂载NavigateComplete2和OnQuit事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void NavigateComplete2(object pDisp, ref object URL)
{
    string url = URL as string;
    if (url.IndexOf("about:blank") >= 0)
    {
        return;
    }
    if (visitHists.Count > 0)
    {
        VisitHist currentHist = visitHists[visitHists.Count - 1];
        if (currentHist.VisitUrl != url)
        {
            currentHist.EndTime = System.DateTime.Now;
        }
        else
        {
            return;
        }
    }
    VisitHist newHist = new VisitHist();
    newHist.StartTime = System.DateTime.Now;
    newHist.VisitUrl = url;
    visitHists.Add(newHist);
}

private void OnQuit()
{
    if (visitHists.Count > 0)
    {
        VisitHist currentHist = visitHists[visitHists.Count - 1];
        currentHist.EndTime = System.DateTime.Now;
    }

    // 输出统计记录

开始编译,然后就可以在bin目录下找到项目的dll文件了。在Console中使用regasm /codebase "UrlTrack.dll"注册dll。打开注册表,在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects可以看到多出了一个子项{E90DA13B-117A-4178-8111-0F712DA09FF9}。

需要注意的是,需要将AssemblyInfo.cs文件中的ComVisible属性设为true,否则在注册BHO时会得到这样的信息:

1
RegAsm : warning RA0000 : No types were registered.

更多的BHO资料可以看Browser Extensions

[1] 在Windows操作系统上有两种浏览器:资源浏览器(Explorer.exe,应用于文件系统)和Internet浏览器(IEXPLORE.EXE,应用于互联网资源)。

代码下载:https://github.com/dohkoos/UrlTrack

XRC和动态子菜单

1、什么是XRC

XRC是基于XML的资源系统。它的基本出发点是将界面布局和程序逻辑分离,即将界面布局代码保存在分离的XML文件中,在程序中不涉及控件的创建和布局,只需要加载相应的资源并处理事件绑定即可。

2、XRC文件格式

1
2
3
4
5
6
<?xml version="1.0"?>
<resource version="2.3.0.1">
    <object class="wxFrame" name="ID_MAIN_FRAME">
        <size>200, 300</size>
    </object>
</resource>

3、XRC文件中菜单资源的相关属性

属性 描述
wxMenuBar style Menu bar style: wxMB_DOCKABLE
wxMenu style Menu style: wxMENU_TEAROFF
label Text: label of the menu.
help Text: displayed help string.
wxMenuItem style Menu style: wxMENU_TEAROFF
label Text: label of the menu.
accel Text: accelerator associated to this item ( Alt-X for example ).
help Text: displayed help string.
radio bool value(0/1): 1 if this item is a radio menu item.
checkable bool value(0/1): 1 if this item is a check menu item.
enabled bool value(0/1): 1 if this item is initially enabled.
checked bool value(0/1): 1 if this (check) item is initially checked.
bitmap Text: path to a bitmap to draw at the left of the item.

4、使用XRC创建菜单

创建一个包含菜单布局信息的XML资源文件MenuBar.xrc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8" ?>
<resource version="2.3.0.1" xmlns="http://www.wxwindows.org/wxxrc">
    <object class="wxMenuBar" name="ID_MENUBAR">
        <object class="wxMenu" name="ID_MENU_FILE">
            <label>&File</label>
            <object class="wxMenuItem" name="wxID_CLOSE">
                <label>E&xit</label>
                <accel>Ctrl+Q</accel>
                <help>Quit the application</help>
            </object>
        </object>
        <object class="wxMenu" name="ID_MENU_VIEW">
            <label>&View</label>
        </object>
        <object class="wxMenu" name="ID_MENU_TOOLS">
            <label>&Tools</label>
            <object class="wxMenuItem" name="wxID_OPTIONS">
                <label>&Options...</label>
            </object>
        </object>
        <object class="wxMenu" name="ID_MENU_HELP">
            <label>&Help</label>
            <object class="wxMenuItem" name="wxID_CHECKFORUPDATES">
                <label>Check for &Updates...</label>
            </object>
            <object class="separator" />
            <object class="wxMenuItem" name="wxID_ABOUT">
                <label>&About...</label>
            </object>
        </object>
    </object>
</resource>

加载资源文件:

1
2
3
4
5
6
bool MainApp::OnInit()
{
    wxXmlResource* pResource = wxXmlResource::Get();
    pResource->AddHandler(new wxMenuBarXmlHandler);
    pResource->AddHandler(new wxMenuXmlHandler);
    pResource->Load(wxT("resources/MenuBar.xrc"));

初始化资源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MainFrame::MainFrame(const wxString& title) : wxFrame(NULL, wxID_ANY, title)
{
    m_menuBar = NULL;
    InitMenuBar();
}

bool MainFrame::InitMenuBar()
{
    if (m_menuBar)
    {
        SetMenuBar(NULL);
        delete m_menuBar;
    }
    // Initialize the resource system
    m_menuBar = wxXmlResource::Get()->LoadMenuBar(wxT("ID_MENUBAR"));
    if (!m_menuBar)
    {
        wxLogError(wxT("Cannot load main menu from resource file"));
        return false;
    }
    SetMenuBar(m_menuBar);
    return true;
}

5、在XRC菜单上添加动态子菜单

在资源文件中添加一个新的菜单项(wxID_LANGUAGES):

1
2
3
4
5
6
7
8
9
10
11
<object class="wxMenu" name="ID_MENU_VIEW">
    <label>&View</label>
    <object class="wxMenu" name="wxID_LANGUAGE">
        <label>&Language</label>
        <object class="wxMenuItem" name="wxID_LANGUAGES">
            <label>Get Additional/Update language pack</label>
            <help>Downloading Additional/Update language pack</help>
        </object>
        <object class="separator" />
    </object>
</object>

创建动态子菜单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Initialize the resource system
m_menuBar = wxXmlResource::Get()->LoadMenuBar(wxT("ID_MENUBAR"));
if (!m_menuBar)
{
    wxLogError(wxT("Cannot load main menu from resource file"));
    return false;
}
/*
 * 这里很奇怪,在XRC文件wxID_LANGUAGE的类型明明是wxMenu,可在这里确只能用wxMenuItem。
 * 查看wxWidgets源代码发现XRC系统只会把最上层的class为wxMenu的object创建为wxMenu对象。
 * 其它的则都被创建成了wxMenuItem对象。
 */
// 使用XRCID方法获取控件ID,创建动态子菜单
wxMenuItem* menuItem = m_menuBar->FindItem(XRCID("wxID_LANGUAGE"));
if (menuItem)
{
    wxMenu* subMenu = menuItem->GetSubMenu();
    subMenu->AppendRadioItem(wxID_LANGUAGE_LOWEST + 1, wxT("English"));
    subMenu->AppendRadioItem(wxID_LANGUAGE_LOWEST + 2, wxT("Chinese(Simplified)"));
}
SetMenuBar(m_menuBar);
return true;

代码下载

如何实现一个可拖动的无标题栏窗口

无标题栏窗口的实现很简单。先将窗口从wxMiniFrame继承,然后在窗口的构造函数中设置一下窗口的样式。

1
2
3
4
5
MainFrame::MainFrame(const wxPoint& pos, const wxSize& size)
    : wxMiniFrame(NULL, wxID_ANY, wxEmptyString, pos, size)
{
    SetWindowStyleFlag(wxFRAME_NO_TASKBAR | wxNO_BORDER);
}

但是,窗口的移动通常都是通过鼠标点住标题栏拖动窗口来实现的,那么现在没有了标题栏,该如何移动窗口呢?我们知道,当鼠标拖动窗口时,它在窗口中的位置是始终不变的。所以如果能够在鼠标移动过程中,通过改变窗口在桌面上的坐标,并且始终保持鼠标相对于窗口的坐标不变,即可实现鼠标的拖动效果。

在具体的设计中,先在鼠标的MouseDown事件中记录下鼠标相对于窗口的偏移,在鼠标的MouseMove事件中根据鼠标在桌面上的位置实时设置窗口的位置,即可达到鼠标拖动窗口的操作。利用此方法实现鼠标拖动,与常规的标题栏鼠标拖动在效果上有一点区别。通过标题栏拖动时,鼠标移动过程中不重画窗口,只有松开鼠标后才在固定位置重画窗口,因此其速度较快。而采用本方法的拖动过程中,每移动一步都需要重画窗口,因此对速度稍有影响,在慢一些的机器上可能会出现轻微的拖尾现象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void MainFrame::OnMouseMove(wxMouseEvent& event)
{
    if (event.Dragging() && event.LeftIsDown())
    {
        wxPoint pt = ClientToScreen(event.GetPosition());
        int x = pt.x - m_delta.x;
        int y = pt.y - m_delta.y;
        Move(x, y);
    }
}

void MainFrame::OnMouseLeftDown(wxMouseEvent& event)
{
    CaptureMouse();
    wxPoint pt = ClientToScreen(event.GetPosition());
    wxPoint origin = GetPosition();
    int dx = pt.x - origin.x;
    int dy = pt.y - origin.y;
    m_delta = wxPoint(dx, dy);
}

void MainFrame::OnMouseLeftUp(wxMouseEvent& WXUNUSED(event))
{
    if (HasCapture())
    {
        ReleaseMouse();
    }
}

代码下载

程序只运行一个实例,并将前一个实例提到前台

wxWidgets提供了一个用来检测是否只有一个实例在运行的wxSingleInstanceChecker类。为了检测程序只运行一个实例,你可以在程序运行之初使用该类创建一个m_check对象,这个对象将存在于程序的整个生命周期。然后就可以在OnInit函数中调用其IsAnotherRunning函数检测是否已经有别的实例在运行。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool MainApp::OnInit()
{
    wxString name = wxString::Format(wxT("MainApp-%s"), wxGetUserId().GetData());
    m_checker = new wxSingleInstanceChecker(name);
    if (m_checker->IsAnotherRunning())
    {
        wxLogError(wxT("Another program instance is already running, aborting."));
        delete m_checker;
        return false;
    }
    // more initializations
    return true;
}

int MainApp::OnExit()
{
    delete m_checker;
    return 0;
}

注意:上面使用了wxGetUserId()来构建实例名,这表示允许不同的用户能同时运行程序的一个实例。如果不这样,那么程序就只能被一个用户运行一次。

但是,如果你想把旧的实例提到前台,或者想使旧的实例打开传递给新的实例的作为命令行参数的文件,该怎么办呢?一般来说,这需要在这两个实例间进行通讯。我们可以使用wxWidgets提供的进程间通讯类来实现。

在下面的实例中,我们将实现程序多个实例间的通讯,以便允许第二个实例请求第一个实例将自己带到前台以提醒用户它已经在运行。下面的代码实现了一个连接类,这个类将被两个实例使用。一个服务器类被旧的实例使用,以便监听新的实例的连接请求。一个客户端类被新的实例使用,以便和旧的实例进行通讯。

1
2
3
4
5
class AppServer : public wxServer
{
public:
    virtual wxConnectionBase* OnAcceptConnection(const wxString& topic);
};
1
2
3
4
5
class AppClient : public wxClient
{
public:
    virtual wxConnectionBase* OnMakeConnection();
};
1
2
3
4
5
6
class AppConnection : public wxConnection
{
public:
    virtual bool OnExecute(const wxString& WXUNUSED(topic), wxChar* WXUNUSED(data),
                          int WXUNUSED(size), wxIPCFormat WXUNUSED(format));
};

当有新的实例(作为Client)进行连接请求时,旧的实例(作为Server)中的OnAcceptConnection函数首先检查旧的实例中没有任何模式对话框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
wxConnectionBase* AppServer::OnAcceptConnection(const wxString& topic)
{
    if (topic.Lower() == wxT("only-one"))
    {
        wxWindowList::Node* node = wxTopLevelWindows.GetFirst();
        while (node)
        {
            wxDialog* dialog = wxDynamicCast(node->GetData(), wxDialog);
            if (dialog && dialog->IsModal())
            {
                return false;
            }
            node = node->GetNext();
        }
        return new AppConnection();
    }
    else
    {
        return NULL;
    }
}

OnExecute函数是一个回调函数,在新的实例对其连接对象(由AppConnection创建的对象)调用Execute函数时被调用。OnExecute函数可以有一个空的参数,这表示它只要将自己提到前台就可以了。

1
2
3
4
5
6
7
8
9
10
11
bool AppConnection::OnExecute(const wxString& WXUNUSED(topic), wxChar* WXUNUSED(data),
                              int WXUNUSED(size), wxIPCFormat WXUNUSED(format))
{
    wxFrame* frame = wxDynamicCast(wxGetApp().GetTopWindow(), wxFrame);
    if (frame)
    {
        frame->Restore();    // 必须要有这句,不然当主窗口最小化时,就不能被提到前台
        frame->Raise();
    }
    return true;
}

接下来我们还需要修改OnInit()函数。当没有别的实例在运行时,这个实例需要将自己设置为Server,等待别的实例的连接请求,如果已经有实例在运行,那么就创建一个和那个实例的连接,请求那个实例将程序的主窗口提到前台。下面的修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
bool MainApp::OnInit()
{
    wxString name = wxString::Format(wxT("MainApp-%s"), wxGetUserId().GetData());
    m_checker = new wxSingleInstanceChecker(name);
    if (!m_checker->IsAnotherRunning())
    {
        m_server = new AppServer();
        if (!m_server->Create(wxT("wxMainApp")))
        {
            wxLogDebug(wxT("Failed to create an IPC service."));
            return false;
        }
    }
    else
    {
        AppClient* client = new AppClient();
        wxString hostName = wxT("localhost");
        wxConnectionBase* conn = client->MakeConnection(hostName, wxT("wxMainApp"), wxT("only-one"));
        if (conn)
        {
            conn->Execute(wxEmptyString);
            conn->Disconnect();
            delete conn;
        }
        else
        {
            wxString msg = wxT("Sorry, the existing instance may be too busy to respond.\nPlease close any open dialogs and retry.");
            wxMessageBox(msg, wxT("wxMainApp"), wxICON_INFORMATION | wxOK);
        }
        delete client;    // 如果没有这句,在运行Debug版本时就会显示如下图的警告
        return false;
    }
    // more initializations
    return true;
}

wxdde-alert

释放实例资源

1
2
3
4
5
6
int MainApp::OnExit()
{
    delete m_checker;
    delete m_server;
    return 0;
}

遇到的问题:

1、调用wxGetApp时编译错误

在调用wxGetApp()时可能会有编译错误,提示说“identifier not found”。这可以通过在App类后加上一行DECLARE_APP(XXXApp)来解决。

2、引入ipc.h和wx.h时的顺序问题

运行第二个实例的时候,发现它总是会挂起在MakeConnection处,查看进程可以看到有两个实例在运行。在网上找了n久,只在wxWidgets Forum上发现有提到这个问题(Windows service using wxWidgets ipc),可是也没有提到如何解决。只能靠自己啦,经过对程序的一步步排除,终于发现是因为引入头文件时将ipc.h放在wx.h之前的原因,掉换引入头文件的顺序后问题被解决。

代码下载

短信分拆算法

今天,突然心血来潮,写了一段分拆短信的代码。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SMSHelper {
    private static final int SMS_MAX_LENGTH = 70;

    public static List split(String msg) {
        if (msg.length() <= SMS_MAX_LENGTH) {
            throw new IllegalArgumentException("被分拆的短信长度必须大于70个字符");
        }

        List<String> list = new ArrayList<String>();
        int i = 0, start = 0;
        while (true) {
            i++;
            String prompt = "(第" + i + "条)";
            int remain = SMS_MAX_LENGTH - prompt.length();
            if (start + remain > msg.length()) {
                list.add(prompt + msg.substring(start));
                break;
            }
            list.add(prompt + msg.substring(start, start + remain));
            start += remain;
        }
        return list;
    }
}

进度条的再思考

英文原文:http://chrisharrison.net/projects/progressbars/ProgBarHarrison.pdf

摘要

进度条在现代用户界面中是普遍存在的。通常,线性函数的使用使得条的进度和已经完成多少工作成正比。然而,许多因素会导致进度条以非线性的速率前进。此外,人类以一种非线性的方式感知时间。这篇论文探讨各种进度条行为在用户感知过程持续时间上的影响。结果被用来提出一些设计考量——可以使进度条看起来更快,最终改善用户的计算体验。

简介

大多数软件包使用进度条显示一个持续过程的状态。用户依靠进度条来验证操作进展顺利,以及估算它的完成时间。通常,线性函数的应用使得进度条的前进和已经完成工作的数量成正比。然而,为复杂的或多阶段的过程估算进度可能是困难的。不同的磁盘,内存,处理器,带宽和其它因素使这个问题进一步复杂化。因而,进度条经常表现出非线性的行为,例如加速、减速和停顿。

再者,人类不以线性的方式感知时间的流逝。再加上进度条的不规则行为,导致人类感知过程持续时间的不同。了解哪些行为在感知上缩短或延长过程持续时间可以被用来改变进度条让它看起来更快,尽管实际的持续时间保持不变。这篇论文描述一个实验,它试图识别在用户感知进度条行为中的模式。然后结果被分析去划分在感知上加速或减速过程执行的行为。我们得出一些设计建议,它们可以被应用于使用进度条的应用,有助于一种总体上反应更灵敏、令人愉悦的和以人为中心的计算体验。

相关工作

Myers调查研究了图形用户界面中进度指示器对用户体验的影响。他得出用户在长期的任务过程中对进度指示器有着强烈的偏好,并且,总的来说,发现它们是有用的。Conn在《Time Affordances: The Time Factor in Diagnostic Usability Heuristics》一文中探讨了时间可见性的概念,文中还列举了一系列理想的进度条的属性。范例给用户提供了一个准确易懂的方法以帮助测量交互系统中的进度。Conn也定义了其它概念——时间容忍窗口,它是用户在判定任务没有取得足够进展前愿意等待的时间长度。Conn接着描述了可以被应用于设置用户期望进行更长等待的预测算法,本质上用一个非线性方法报告进度来增强用户体验。

Fredrickson等人提出持续时间对情感体验多么令人满意的评估影响不大(持续时间忽视)。相反,在体验期间和在体验结束时(峰端效应)感知受到显著特征(好的和坏的)的影响最严重。出现这种情况是因为人类不能以一种一致的和线性的方式记住体验,而是有选择地和带着各种偏见地回忆事件。持续时间忽视(duration neglect)和峰端效应(peak-and-end effects)可以在各种各样的领域中看到,包括医学、经济学、广告和人机交互。

实验

我们识别并开发了8个非线性函数,它们体现不同的进度行为。包含一个线性函数作为比较的基线。Table 1和Figure 1描述了每个进度函数的行为。为测试这些函数的人类感知,开发了一个实验应用,它同时给用户提供像Figure 2这样的两个进度条。这些进度条逐次运行:当第一个进度条完成时第二个进度条自动开始。每个进度条充当控件的持续时间保持在一个恒定的5.5秒。界面提供三个响应按钮,允许用户选择是否第一个或第二个进度条显示更快些或者它们在持续时间上是相等的。另一个按钮允许用户在继续随后的一对进度条之前重放每次测试。一旦提供了答案,下一次测试就会被启动。响应和重放按钮可以在任何时候被按下。

progress-bar-table-1

基于Java的应用运行在一台12”显示器1024x768分辨率的Apple笔记本上。进度条是使用Java Graphics2D原语定制的,600x50像素大小(大约1.2cm x 14.3cm)。一套着色和命名方案被应用于更好地可视化地通知用户:运行的进度条显示成蓝色并且题为“running”,而完成的进度条染成绿色并且题为“finished”。用户通过带有一个集成的、单独的鼠标按钮的触摸板和界面交互。

比较9个进度函数的所有不同顺序对需要81次测试。最初的试点测试显示,在大约50套进度条后用户发现任务是相当乏味的并开始失去兴趣。为了保持主题的关注和确保响应完整的高水平,我们决定给每个用户呈现9个进度函数的所有组合(36次测试)以及与自己配对的函数组合(9次测试)总计45次测试。这保持总的任务时间在15分钟以下。呈现的顺序在两个方面被抵消。首先,对每对用户来说45次测试的序列是随机选择的。其次,在每对用户中,每次测试的呈现顺序是相反的(也就是说,如果配对中第一个用户看见Linear/Power,第二个用户将看见Power/Linear)。

progress-bar-figure-1

我们从两个大的计算机研究实验室招募了平均年龄约为37岁的22名参与者(14名男性,8名女性)。实验在参与者的办公室里进行。简易比较界面的一个简短口头解释被给出。参与者被告知进度条可能以不同速率前进,他们应该选择他们认为更快的进度条或者如果速度相同就选择等于。

progress-bar-figure-2

分析和结果

参与者往往偏好(也就是说,认为更快)第一次看到的那个函数。在990对比较中,第一个函数被优先选择376次(38%),第二个262次(26%),没有任何偏好352次(36%)。这一发现被随后讨论的卡方检验结果所支持。

参与者在9个函数中有着强烈的偏好。对函数的任何配对比较,我们指派一个+1的偏好分数如果第一个函数被优先选择,如果第二个函数被优先选择则是-1,如果参与者没有偏好就是0。Table 2显示了36个函数对中每一个的平均偏好分数。例如,在22个Slow Wavy和Fast Wavy的比较中(每个出现11次),10名参与者优先选择Fast Wavy,5名优先选择Slow Wavy,7名认为函数是相等的。因此,平均偏好分数是(10 – 5) / 22 = 0.23。表的行和列根据增长的总体偏好排序。粗体表示统计显著性从0开始在0.05水平上使用每个函数是同等可能被优先选择的零假设的一个双边符号测试。

progress-bar-table-2

使用Table 2中的平均偏好分数,我们为9个进度函数生成一个粗略的偏好排序,如Figure 3所示。

progress-bar-figure-3

为有效地组合跨单元信息,在控制呈现顺序时,我们使一个logistic回归模型与偏好已给出的638个案例相拟合。偏好Function i胜于Function j假定函数Function i被第一次看到的概率被建模为

progress-bar-formula

Hosmer-Lemeshow卡方检验(8.87和自由度7)未能显示模型拟合的不足。参数α据估计是带有标准误差0.09的0.42,反映了参与者更偏好他们看见的第一个函数的倾向性。估计的α度量在函数中的相对偏好。因为概率仅仅依赖α间的差异,我们把为Linear估计的α固定在0(Figure 4)。α间差异的标准误差介于0.28和0.37。

progress-bar-figure-4

9个函数干净利落地聚成三组(Figure 4):3个被认为比Linear更慢,4个被认为接近Linear,2个被认为比Linear更快。所有三组间的不同是显著的但在组内不一定显著。每个函数的α和其它任何群集中的每个函数在0.05水平上差异显著。被认为比Linear更快的2个函数Power和Fast Power都是指数函数,在接近过程的结尾以更快的进度发生。Slow Wavy、Fast Wavy和Late Pause,仅有的在接近过程结束有停顿的函数,都被认为比Linear更慢。

三个常规发现解释了估算模式,它符合前面提到的峰端效应。首先,参与者认为带有停顿的进度条需要更长的时间去完成(峰值效应)。其次,加速的进度被强烈地青睐。当位于过程即将结束时两种效应的后者有一个夸张的感知影响(末端效应)。有趣的是,两个因素似乎结合在Early Pause情况下,使它基本上等同于偏好的Linear函数。

论述

尽管我们的结果可以被用来增强整个系统的进度条,但有许多情况修改进度行为似乎不合适。一般而言,带有已知静态完成条件和稳定进度的过程不是好的候选者——标准进度条能有效地、精确地可视化这些。另外,这些过程类型往往很少被停顿或其它负面进度行为影响(足以使它们经常伴随着精确的时间估算)。这种过程类型的例子包括拷贝一个文件到磁盘,扫描一张照片,或者播放一个音频文件。

然而,带有动态完成条件和粗略估计持续时间的进度条(比如,碎片整理硬盘驱动器)可以在两个重要方面被加强。首先,因为用户似乎对停顿特别是一个操作即将结束时有种强烈的厌恶,进度条可以被设计成用于弥补这种行为。一个智能的进度条可以缓存进度当操作首先开始缓解以后的负面进度行为(比如,停顿或者迟缓)时。其次,进度可以在开始时被低估和在即将结束时被加速,提供一个迅速结束的感觉在我们的实验中是被用户高度青睐的。

感知增强也可以被集成到多阶段过程的设计中,例如软件的安装。我们的结果表明用户最愿意在一个操作的开始容忍负面的进度行为(比如,停顿和不一致的进度)。因此,过程阶段可以安排那些更慢的或多变的操作最先完成。例如,如果安装程序的一部分需要从远程服务器获取更新,而网络连接可能是不稳定或不可靠的,最好在安装顺序的早期运行这个阶段。更新自己总是可以被稍后应用,因为它们运行在本地,有更多的可预测行为。

总结

不同的进度条行为在用户感知进度持续时间上似乎有显著影响。通过最小化负面行为和结合正面行为,可以有效地使进度条及其相关过程看起来更快。此外,如果一个多阶段操作的元素可以被重新排列,它可能会用更令人满意和似乎更快的序列重新排序阶段。

未来工作

在此次实验中,所有进度条在5.5秒内完成。然而许多使用进度条的操作有相当长的运行时。如果我们的发现扩展到其它持续时间,调查研究应该会很有趣。此外,其它进度行为和行为组合的研究可能揭示新的感知影响。同时,一个自适应实验可以被进行,在此实验中,单独的进度条时间将被动态调整到所有函数在持续时间上被认为相等的一个状态。这将允许相关感知的变体被定量评估。