Board index » Visual Studio » How do you interactively write to a CEditView Window

How do you interactively write to a CEditView Window

Visual Studio352
(Newbie): I am trying to output status and diagnostic information to a

CEditView Window and allow users input annotations to the same window. The

User input is handled by Windows. How do I output to the window? How do I

ensure that program output is appended to existing text? DrawText erases the

screen and outputs to (0,0). What is the magic? (Please)



skidmarks


-
 

Re:How do you interactively write to a CEditView Window

On Thu, 17 Apr 2008 10:34:00 -0700, skidmarks

<skidmarks@discussions.microsoft.com>wrote:



Quote
(Newbie): I am trying to output status and diagnostic information to a

CEditView Window and allow users input annotations to the same window. The

User input is handled by Windows. How do I output to the window? How do I

ensure that program output is appended to existing text? DrawText erases the

screen and outputs to (0,0). What is the magic? (Please)



You don't draw to CEditView windows. Instead, you set the text of the

control using CEdit functions (see GetEditCtrl), and the underlying EDIT

control draws itself.



--

Doug Harrison

Visual C++ MVP

-

Re:How do you interactively write to a CEditView Window

skidmarks,



Quote
(Newbie): I am trying to output status and diagnostic information to a

CEditView Window and allow users input annotations to the same window. The

User input is handled by Windows. How do I output to the window? How do I

ensure that program output is appended to existing text? DrawText erases

the

screen and outputs to (0,0). What is the magic? (Please)



If I understand what you are saying, this approach is a nightmare for even

the most experienced MFC developer.



When you use CEditView, you are saying that you will let an EDIT control

handle painting within your view window in order to save you the trouble. If

you decide to try and draw to that same window, then you and the EDIT

control will be overwriting each other and things will not work.



There are a couple of approaches to handling this:



1. Eliminate the EDIT control (don't use CEditView) and draw all the text

and handle all the editing yourself. As you might guess, this is a major

undertaking. The advantage is that you can then display text however and

where you like. This is the approach a program like Microsoft Word would

take.



2. Combine an EDIT control and your own window within the view. If you don't

give your windows a border, you can even make it appear as though everything

is part of the same window. Note that I would not use CEditView when trying

something like this.



3. Finally, if all you want to do is programatically add text to the EDIT

control, if you check the CEditView docs, you can use GetEditCtrl() method

to get a reference to the edit control. You can then modify the contents of

the EDIT control view the CEdit reference that is returned. You then allow

the EDIT control to handle painting those changes to your view window. To

insert text at a particular location in the EDIT control, you can use the

SetSel() and ReplaceSel() methods.



Unfortunately, all but the last item are fairly advanced techniques.



--

Jonathan Wood

SoftCircuits Programming

www.softcircuits.com">www.softcircuits.com



-

Re:How do you interactively write to a CEditView Window

When I was a newbie I thought edit windows would be good for this purpose. I quickly

learned that they are largely unsuitable for this purpose, and over the years, designed my

Logging ListBox control, which is a lot easier to use, much more effective, and does not

have the endless set of problems that an edit control has.



You can download it from my MVP Tips site.

joe



On Thu, 17 Apr 2008 10:34:00 -0700, skidmarks <skidmarks@discussions.microsoft.com>wrote:



Quote
(Newbie): I am trying to output status and diagnostic information to a

CEditView Window and allow users input annotations to the same window. The

User input is handled by Windows. How do I output to the window? How do I

ensure that program output is appended to existing text? DrawText erases the

screen and outputs to (0,0). What is the magic? (Please)



skidmarks



Joseph M. Newcomer [MVP]

email: newcomer@flounder.com

Web: www.flounder.com">www.flounder.com

MVP Tips: www.flounder.com/mvp_tips.htm">www.flounder.com/mvp_tips.htm

-

Re:How do you interactively write to a CEditView Window

CListBox works EXCEEDINGLY WELL for this purpose, requires no specialized knowledge other

than turning off the "sort" flag in the listbox. But my Logging ListBox allows for copy,

cut, well-behaved scrolling, writing to a log file (immediately or on demand) and several

other features, and is VERY easy to use once it is set up. I might do something like



log->PostMessage(UWM_LOG, new TraceFormatComment(TraceEvent::None, _T("The answer is now

%d"), answer)));

although for localization I'm more likely to write

log->PostMessage(UWM_LOG, new TraceFormatComment(TraceEvent::None, IDS_ANSWER, answer),

answer));



It supports using STRINGTABLE ids for messages, and as shown by the use of PostMessage

allows messages to be logged from separate threads quite readily. All you have to do is

put an appropriate user-defined message handler in the window and identify the window to

which the logging is done. Or call the AddString method directly.

joe



On Thu, 17 Apr 2008 12:03:12 -0600, "Jonathan Wood" <jwood@softcircuits.com>wrote:



Quote
skidmarks,



>(Newbie): I am trying to output status and diagnostic information to a

>CEditView Window and allow users input annotations to the same window. The

>User input is handled by Windows. How do I output to the window? How do I

>ensure that program output is appended to existing text? DrawText erases

>the

>screen and outputs to (0,0). What is the magic? (Please)



If I understand what you are saying, this approach is a nightmare for even

the most experienced MFC developer.



When you use CEditView, you are saying that you will let an EDIT control

handle painting within your view window in order to save you the trouble. If

you decide to try and draw to that same window, then you and the EDIT

control will be overwriting each other and things will not work.



There are a couple of approaches to handling this:



1. Eliminate the EDIT control (don't use CEditView) and draw all the text

and handle all the editing yourself. As you might guess, this is a major

undertaking. The advantage is that you can then display text however and

where you like. This is the approach a program like Microsoft Word would

take.



2. Combine an EDIT control and your own window within the view. If you don't

give your windows a border, you can even make it appear as though everything

is part of the same window. Note that I would not use CEditView when trying

something like this.



3. Finally, if all you want to do is programatically add text to the EDIT

control, if you check the CEditView docs, you can use GetEditCtrl() method

to get a reference to the edit control. You can then modify the contents of

the EDIT control view the CEdit reference that is returned. You then allow

the EDIT control to handle painting those changes to your view window. To

insert text at a particular location in the EDIT control, you can use the

SetSel() and ReplaceSel() methods.



Unfortunately, all but the last item are fairly advanced techniques.

Joseph M. Newcomer [MVP]

email: newcomer@flounder.com

Web: www.flounder.com">www.flounder.com

MVP Tips: www.flounder.com/mvp_tips.htm">www.flounder.com/mvp_tips.htm

-

Re:How do you interactively write to a CEditView Window





Quote
3. Finally, if all you want to do is programatically add text to the EDIT

control, if you check the CEditView docs, you can use GetEditCtrl() method

to get a reference to the edit control. You can then modify the contents of

the EDIT control view the CEdit reference that is returned. You then allow

the EDIT control to handle painting those changes to your view window. To

insert text at a particular location in the EDIT control, you can use the

SetSel() and ReplaceSel() methods.



--

Jonathan Wood

SoftCircuits Programming

www.softcircuits.com">www.softcircuits.com







Johnathan;



I just looked at VC Help.

1. The SetSel function allows specification of the start byte

and end byte in the first parameter DWORD. This gives a 1

6-bits *(65,526) reference in the Edit Window. Isn't this

restrictive?

2, Thanks. You've got my little grey cells going.



To decrease simultaneous writes can I use the buffer lock/unlock functions?



skidmarks

-

Re:How do you interactively write to a CEditView Window

skidmarks,



Quote
1. The SetSel function allows specification of the start byte

and end byte in the first parameter DWORD. This gives a 1

6-bits *(65,526) reference in the Edit Window. Isn't this

restrictive?



I'm not sure where you read that. The CEdit::SetSel() method takes two ints,

not a combined DWORD.



Internally, the start position goes in wParam and the end position goes in

lParam. So, no, that limitation does not exist. At least not with 32-bit

Windows.



--

Jonathan Wood

SoftCircuits Programming

www.softcircuits.com">www.softcircuits.com



-

Re:How do you interactively write to a CEditView Window

Joseph;



Thank you for helping the hapless. I do have a comment based on a previous

comment on using SetSel and ReplaceSel (which maybe you can comment on). In

using SetSel followed by ReplaceSel it seems to work, except that I can't

seem to append a leading carriage return ("\n") to force the output to the

next line. What I have tried to do is to prevent an issue with an attempt to

write by the user and the program from causing problems by using LockBuffer

and UnlockBuffer. The question I have is why this ReplceSel and SetSel won't

work, or maybe, work correctly? What am I missing?



I am looking at your MVP code for logging and will probably gratefully adopt

it for perhaps this, but most certainly for other things. The residual

question is can I change the project to use CListBox for the project view and

incorporate your code? This seems like a lot of work.



And as always, you have left jewels.



Thanks



skidmarks

-

Re:How do you interactively write to a CEditView Window

To put a newline sequence in, you have to use "\r\n". One of the problems of using the

SetSel/ReplaceSel is that if you scroll the edit control back to see something, it rips

the focus away from where you are and leaves you at the end of the control again. This is

very disruptive to the user (my Logging ListBox, for example, decides if it should

auto-scroll or not scroll based on the position you are looking at; if you scroll back it

won't rip your attention back to the end just to do the append)



As to how much work it is, that's hard to judge. Typically, I will do something like

c_Log.AddString(new TraceComment(...)));

if I'm not using threads, so if you had



c_Log.SetSel(...at end...);

c_Log.ReplaceSet(_T("\r\nThis is a message"));



I would write



c_Log.AddString(new TraceComment(_T("This is a message")));



so it isn't all that much effort to convert. I've done it a couple times when clients

sent code that was using a CEdit to log data. Depends on how many instances you have, and

whether or not you are doing partial-line output. Most forms have a TraceSomething and a

TraceFormatSomething, where the TraceSomething usually has a form with a UINT and with an

LPCTSTR, so you can easily use the STRINGTABLE, and the TraceFormatSomething usually has a

form with a UINT and an LPCSTR as a formatting string and then use the usual varargs to

supply arguments.

joe

On Mon, 21 Apr 2008 15:26:10 -0700, skidmarks <skidmarks@discussions.microsoft.com>wrote:



Quote
Joseph;



Thank you for helping the hapless. I do have a comment based on a previous

comment on using SetSel and ReplaceSel (which maybe you can comment on). In

using SetSel followed by ReplaceSel it seems to work, except that I can't

seem to append a leading carriage return ("\n") to force the output to the

next line. What I have tried to do is to prevent an issue with an attempt to

write by the user and the program from causing problems by using LockBuffer

and UnlockBuffer. The question I have is why this ReplceSel and SetSel won't

work, or maybe, work correctly? What am I missing?



I am looking at your MVP code for logging and will probably gratefully adopt

it for perhaps this, but most certainly for other things. The residual

question is can I change the project to use CListBox for the project view and

incorporate your code? This seems like a lot of work.



And as always, you have left jewels.



Thanks



skidmarks

Joseph M. Newcomer [MVP]

email: newcomer@flounder.com

Web: www.flounder.com">www.flounder.com

MVP Tips: www.flounder.com/mvp_tips.htm">www.flounder.com/mvp_tips.htm

-

Re:How do you interactively write to a CEditView Window

Thanks Joe.



skidmarks

-

Re:How do you interactively write to a CEditView Window

Oops, that should be



new TraceComment(TraceEvent::None, _T("whatever"));



because I allow an integer to be displayed in the left column; TraceEvent::None says

"don't display an integer, leave the column blank"

joe



On Tue, 22 Apr 2008 05:18:01 -0700, skidmarks <skidmarks@discussions.microsoft.com>wrote:



Quote
Thanks Joe.



skidmarks

Joseph M. Newcomer [MVP]

email: newcomer@flounder.com

Web: www.flounder.com">www.flounder.com

MVP Tips: www.flounder.com/mvp_tips.htm">www.flounder.com/mvp_tips.htm

-

Re:How do you interactively write to a CEditView Window

Joe;



I've got a little issue with one of your statements on why not to use

<CEditCtrl>.SetSel and <CEditCtrl>.ReplaceSel. Remember I'm a newbie not so

much trying to make a name for myself as trying how best to do a task.



Your prior post indicated an issue with scrolling. When you scroll the

CEditView window this puts the cursor out of sorts and using the <CEditCtr>

methods leads to putting text in the incorrect position on screen. I tried

that. Works fine - code below. My guess is that the caveat should be to use

<CEdit>.GetBufferLength to determine the placement location.



In any case. are there other issues?



==================================================



// Selecting Menu Item "Test" causes execution



void <CAppView>::OnTestText()

{



static const char Message[] = _T("\r\n\nThe Last Message Is:\r\n");

static const int MsgSz = sizeof(Message);



CEdit& y = GetEditCtrl();

int Lo = GetBufferLength() + 1;

int Hi = GetBufferLength() + MsgSz - 1;

DWORD Wrd = Hi << 16 | Lo;



LockBuffer();

y.SetSel(Wrd);

y.ReplaceSel(Message);

UnlockBuffer();



-

Re:How do you interactively write to a CEditView Window

See bekiw,,,

On Tue, 22 Apr 2008 14:01:00 -0700, skidmarks <skidmarks@discussions.microsoft.com>wrote:



Quote
Joe;



I've got a little issue with one of your statements on why not to use

<CEditCtrl>.SetSel and <CEditCtrl>.ReplaceSel. Remember I'm a newbie not so

much trying to make a name for myself as trying how best to do a task.



Your prior post indicated an issue with scrolling. When you scroll the

CEditView window this puts the cursor out of sorts and using the <CEditCtr>

methods leads to putting text in the incorrect position on screen. I tried

that. Works fine - code below. My guess is that the caveat should be to use

<CEdit>.GetBufferLength to determine the placement location.

****

Well, it's more complex than that. For example, the window flashes a lot. And if you

have scrolled the window back to see some earlier output, as soon as a new line is added,

the window is repositioned to the end, so you have to scroll back again, at which point,

while you are reading, the next line added scrolls you back to the end of the text, and so

on. Miserable to use, and trying to control the flashing and deal with the user resetting

the cursor adds further complexity.



Performance also degrades as the contents get longer and longer, because doing things like

finding the end takes longer and longer, and the ReplaceSel causes a reallocation of the

buffer so the whole buffer has to be copied. Using a list box means never having to say

"reallocate the buffer", so the performance remains essentially constant no matter how

many items you have. My control also allows you to set a maximum limit on the number of

items, and will remove them from the front of the control after the limit is hit.



A simple CListBox is a better choice, but then it won't automatically scroll, although you

can see how I handle this in my control, and use just that much of the control.



It isn't a question of making a name for anything or anyone; it's a question of coming up

with a solution that works well. CEdit just doesn't work well.

****

Quote


In any case. are there other issues?



==================================================



// Selecting Menu Item "Test" causes execution



void <CAppView>::OnTestText()

{



static const char Message[] = _T("\r\n\nThe Last Message Is:\r\n");

static const int MsgSz = sizeof(Message);

****

Why? Note that the first line is erroneous because it uses the obsolete type 'char' but

the initializer is Unicode-aware and will generate a Unicode string in a Unicode build, so

this would fail to compile. You could write

static const TCHAR Message[] = _T("...");

but then sizeof is wrong; the correct way to write it would be

static const int MsgSize = (sizeof(Message) - 1)/sizeof(TCHAR);

note that sizeof() a string will include the NUL character, while what you really want is

the length of the string, not the length of the array. In fact, you should forget the

existence of sizeof(). There is a _countof() defined in VS2005, but you can easily define

it yourself:



#define _countof(x) (sizeof(x) / sizeof( (x)[0]) )



(This is the oversimplified version; there is a much fancier version that is C++

compatible, but the above will do for now)

****

Quote


CEdit& y = GetEditCtrl();

int Lo = GetBufferLength() + 1;

int Hi = GetBufferLength() + MsgSz - 1;

DWORD Wrd = Hi << 16 | Lo;

***

DWORD Wrd = MAKELONG(Lo, Hi);

The code you wrote would be incorrect if GetBufferLength() returned a value>65535.



****

Quote


LockBuffer();

y.SetSel(Wrd);

y.ReplaceSel(Message);

****

This replaces the entire contents, because the entire contents have been selected. Note

that you are using the wrong form of SetSel; you should be calling

y.SetSel(Lo, Hi);

y.ReplaceSel(Message);



which will do the same thing but which will work correctly if the edit control contains

more than 64K characters.



What do LockBuffer/UnlockBuffer do?

joe

****

Quote
UnlockBuffer();

Joseph M. Newcomer [MVP]

email: newcomer@flounder.com

Web: www.flounder.com">www.flounder.com

MVP Tips: www.flounder.com/mvp_tips.htm">www.flounder.com/mvp_tips.htm

-

Re:How do you interactively write to a CEditView Window

What do you mean by "simultanous updates"? There should be no such concept; there is one,

and ONLY one, thread that is permitted to access this control, which is the main GUI

thread, and it cannot perform more than one update at a time, and therefore "simultaneous"

updates are impossible. If you are using threads, you must NOT, repeat NOT, touch any

control owned by the main GUI thread from any secondary thread for any reason. See my

essay on worker threads; a thread would PostMessage the information to be displayed to the

main GUI thread, which would do the update.



Any attempt to manipulate a control directly by any thread other than its owner will lead

to situations in which you can get deadlock.

joe



On Mon, 21 Apr 2008 13:40:22 -0700, skidmarks <skidmarks@discussions.microsoft.com>wrote:



Quote




>3. Finally, if all you want to do is programatically add text to the EDIT

>control, if you check the CEditView docs, you can use GetEditCtrl() method

>to get a reference to the edit control. You can then modify the contents of

>the EDIT control view the CEdit reference that is returned. You then allow

>the EDIT control to handle painting those changes to your view window. To

>insert text at a particular location in the EDIT control, you can use the

>SetSel() and ReplaceSel() methods.



>--

>Jonathan Wood

>SoftCircuits Programming

>www.softcircuits.com">www.softcircuits.com

>

>



Johnathan;



I just looked at VC Help.

1. The SetSel function allows specification of the start byte

and end byte in the first parameter DWORD. This gives a 1

6-bits *(65,526) reference in the Edit Window. Isn't this

restrictive?

2, Thanks. You've got my little grey cells going.



To decrease simultaneous writes can I use the buffer lock/unlock functions?



skidmarks

Joseph M. Newcomer [MVP]

email: newcomer@flounder.com

Web: www.flounder.com">www.flounder.com

MVP Tips: www.flounder.com/mvp_tips.htm">www.flounder.com/mvp_tips.htm

-

Re:How do you interactively write to a CEditView Window



Hey Joe.



"Joseph M. Newcomer" wrote:



Quote


>==================================================

>

>// Selecting Menu Item "Test" causes execution

>

>void <CAppView>::OnTestText()

>{

>

>static const char Message[] = _T("\r\n\nThe Last Message Is:\r\n");

>static const int MsgSz = sizeof(Message);

****

Why? Note that the first line is erroneous because it uses the obsolete type 'char'

--- Unless there is a new C++ standard, 'char' is perfectly legitimate.



Quote
but the initializer is Unicode-aware and will generate a Unicode string in a

Unicode build, ...



I'm using VC 2003 (sigh). Compiles and executes just fine. When the project

was created 'Unicode' was not selected and therefor no issue.



Quote
****

>

>CEdit& y = GetEditCtrl();

>int Lo = GetBufferLength() + 1;

>int Hi = GetBufferLength() + MsgSz - 1;

>DWORD Wrd = Hi << 16 | Lo;

***

DWORD Wrd = MAKELONG(Lo, Hi);

The code you wrote would be incorrect if GetBufferLength() returned a value>65535.



--- Couldn't remember MAKELONG.

--- New about the 64k limit, didn't know how to solve it.

Quote


****

>

>LockBuffer();

>y.SetSel(Wrd);

>y.ReplaceSel(Message);

****

This replaces the entire contents, because the entire contents have been

selected. Note that you are using the wrong form of SetSel; you should be

calling

y.SetSel(Lo, Hi);

y.ReplaceSel(Message);



which will do the same thing but which will work correctly if the edit control

contains more than 64K characters.



I knew about the 64k issues but didn't know there was a solution. My Help

does not include SetSel(Lo, Hi) as an option. I will try it and hope it works.



Quote


What do LockBuffer/UnlockBuffer do?



I'm guessing that it prevents simultaneous writes by the user and the

program into the CEditView buffer.



At the end I think the case for NOT using a CEditView and using a CListBox

is conclusive, final, solid reasoning, and drat!!! As soon as I can I'm going

to change the code and use yours, with accreditation.



Thank you for your time. You have convincingly and conclusively nailed the

idea of using a CEditView into a coffin. Sigh. A programmer's life is never

easy (but it is very rewarding).



skidmarks



Quote
joe

****

>UnlockBuffer();

Joseph M. Newcomer [MVP]

email: newcomer@flounder.com

Web: www.flounder.com">www.flounder.com

MVP Tips: www.flounder.com/mvp_tips.htm">www.flounder.com/mvp_tips.htm



-

Re:How do you interactively write to a CEditView Window

See below...

On Wed, 23 Apr 2008 06:33:00 -0700, skidmarks <skidmarks@discussions.microsoft.com>wrote:



Quote


Hey Joe.



"Joseph M. Newcomer" wrote:



>

>>==================================================

>>

>>// Selecting Menu Item "Test" causes execution

>>

>>void <CAppView>::OnTestText()

>>{

>>

>>static const char Message[] = _T("\r\n\nThe Last Message Is:\r\n");

>>static const int MsgSz = sizeof(Message);

>****

>Why? Note that the first line is erroneous because it uses the obsolete type 'char'

--- Unless there is a new C++ standard, 'char' is perfectly legitimate.

****

Well, the code you wrote is nonsense, because if you write code, it has to be consistent

with reality. Therefore, had you written

static const char Message[] = "\r\nThe Last Message is:\r\n";

it would have merely been poor style using what is now thought of as an obsolete data

type. But what you have above, using char on one side and _T() on the other, is

out-and-out wrong.

****

Quote


>but the initializer is Unicode-aware and will generate a Unicode string in a

>Unicode build, ...



I'm using VC 2003 (sigh). Compiles and executes just fine. When the project

was created 'Unicode' was not selected and therefor no issue.

****

I am not aware that

char x[] = L"abc";



can work because it is not a legal assignment. You can't assign a wchar_t* to a char* or

a wchar_t[] to a char[].



VC2003 produces the following message:



char data[] = L"ABC"; // <= line 6





i:\tests\wct\wct\wct.cpp(6) : error C2440: 'initializing' : cannot convert from 'const

unsigned short [4]' to 'char []'

There is no context in which this conversion is possible

****

Quote


>****

>>

>>CEdit& y = GetEditCtrl();

>>int Lo = GetBufferLength() + 1;

>>int Hi = GetBufferLength() + MsgSz - 1;

>>DWORD Wrd = Hi << 16 | Lo;

>***

>DWORD Wrd = MAKELONG(Lo, Hi);

>The code you wrote would be incorrect if GetBufferLength() returned a value>65535.



--- Couldn't remember MAKELONG.

--- New about the 64k limit, didn't know how to solve it.

****

SetSel has a form that takes two parameters (actually three, with a default BOOL). That

solves it.

****

Quote
>

>****

>>

>>LockBuffer();

>>y.SetSel(Wrd);

>>y.ReplaceSel(Message);

>****

>This replaces the entire contents, because the entire contents have been

>selected. Note that you are using the wrong form of SetSel; you should be

>calling

>y.SetSel(Lo, Hi);

>y.ReplaceSel(Message);

>

>which will do the same thing but which will work correctly if the edit control

>contains more than 64K characters.



I knew about the 64k issues but didn't know there was a solution. My Help

does not include SetSel(Lo, Hi) as an option. I will try it and hope it works.



>

>What do LockBuffer/UnlockBuffer do?



I'm guessing that it prevents simultaneous writes by the user and the

program into the CEditView buffer.

****

I looked it up. According to the MSDN:



CEditView::LockBuffer



Call this member founction to obtain a pointer to the buffer. The buffer should not be

modified.



And what do you do? You modify the buffer! Drop the code.

****

Quote


At the end I think the case for NOT using a CEditView and using a CListBox

is conclusive, final, solid reasoning, and drat!!! As soon as I can I'm going

to change the code and use yours, with accreditation.



Thank you for your time. You have convincingly and conclusively nailed the

idea of using a CEditView into a coffin. Sigh. A programmer's life is never

easy (but it is very rewarding).



skidmarks



>joe

>****

>>UnlockBuffer();

>Joseph M. Newcomer [MVP]

>email: newcomer@flounder.com

>Web: www.flounder.com">www.flounder.com

>MVP Tips: www.flounder.com/mvp_tips.htm">www.flounder.com/mvp_tips.htm

>

Joseph M. Newcomer [MVP]

email: newcomer@flounder.com

Web: www.flounder.com">www.flounder.com

MVP Tips: www.flounder.com/mvp_tips.htm">www.flounder.com/mvp_tips.htm

-