Thursday, October 1, 2009

Pro/WebLink: Replace Drawing Formats

Jürgen from Germany was asking me recently about replacing formats with Pro/WebLink. In the process of providing a solution, I found an interesting combination of disappointments, curiosities, and finally a very nice solution.

SetSheetFormat()

Looking through the API documentation, it's not too hard to find that the method call to replace drawing formats is SetSheetFormat() in the pfcSheetOwner class. One of the primary arguments is the drawing format object in the form of a pfcDrawingFormat object. Unfortunately, this object's class is not obviously documented. The docs say only that its parent class is a pfcModel. So how to you get a drawing format object when it seems to have no methods?

Since it's a subclass of pfcModel, you use pfcBaseSession methods such as OpenFile(), GetModel, and RetrieveModel(). Here's where the J-Link docs are bit better, because they list the methods inherited from parent classes and interfaces, just making it that much more obvious. To use RetrieveModel(), a pfcModelDescriptor object needs to be created which specifies what is to be retrieved and how.

Here is the core sequence of steps, assuming the current model is a drawing:

var session = pfcGetProESession();
var drawing = session.CurrentModel;
var drwSheetNum = drawing.CurrentSheetNumber; // use current sheet

var frmName = "c_size.frm";
var frmSheetNum = 1;
var frmDescr = pfcCreate("pfcModelDescriptor").CreateFromFileName(frmName);
var format = session.RetrieveModel(frmDescr);

var drwModel = null;
drawing.SetSheetFormat(drwSheetNum, format, frmSheetNum, drwModel);
 

Format Folder

You'll notice that the folder where the format is stored was not specified. Why? According to the docs, you can't specify the path to a folder containing a pfcModel when using RetrieveModel(). Although the pfcModelDescriptor object contains a Path property, it is ignored.

How does Pro/Engineer know where to find the format? It just does. Well, if the PRO_FORMAT_DIR config.pro option is set properly, then it just does. Fortunately, this config.pro option can handle folder paths, Intralink 3.x folders, and PDMLink folders. That should cover most scenarios.


Format Sheets

SetSheetFormat() also allows you to specify the sheet number of the format to use (should your format have more than one sheet). Unfortunately, since the pfcDrawing Format object does not implement the pfcSheetOwner interface, the developer has no way to determine how many sheets are in the format. You'll just have to know how many sheet your formats have ahead of time, not a major tragedy.

To be fair this is a Pro/Toolkit limitation first and foremost. The PFC APIs merely inherit Pro/Toolkit's limitations.

Drawings, layouts, and Pro/Report files (who uses these anyway?!) all are defined as pfcModel2D objects, which gives lots of info about sheets, tables, and detail items. Formats, although pretty much the same thing, are not pfcModel2D objects. As a result, we can get none of these things.


Drawing Model

The drawing model is the part or assembly documented on the current sheet. Although this can be specified as the last argument to SetSheetFormat(), we don't have the ability to know what the current model is (at least until WF4). null is used to here to indicate the current drawing model.


Format Tables

Just as with interactive use of Pro/Engineer, when drawing formats are replaced, the drawing tables from the previous format have to be dealt with. Typically, this involves deleting the tables. Since SetSheetFormat() does not provide a mechanism or option for removing the old format tables, this will have to be done with more code.

This is where it gets a little tricky. While there is a CheckIfIsFromFormat() method call that will tell us if a table came from a format, we'll need the table object first. To get the table object, first we'll have to use ListTables() from the pfcTableOwner class to get all of the tables for the entire drawing. ListTables() takes no argument, meaning that we have no way to pre-filter the collection of tables. It really should take a sheet number argument to return only the tables on that sheet. As an alternatives, had the pfcTable class been a pfcDetailItem (in addition to a pfcModelItem), we could have gathered the tables by sheet number using ListDetailItems(), from pfcDetailItemOwner.

var tables = drawing.ListTables();
 
The workflow for CheckIfIsFromFormat() is a bit odd as well. To check if a table is from a format, we need the pfcTableObject, and then we need to specify the sheet number. Now wait a minute, the function returns a boolean so logically the table is from the format or it is not. What does the sheet number have to do with that? Even if the table had been segmented, it's still going to be from the format or not.

if (table.CheckIfIsFromFormat(drwSheetNum) == true) {
// table is from format, delete it
drawing.DeleteTable(table, false);
}
 
This quirkiness turns out to be very useful because it's rather difficult to tell on what sheet the table exists. The pfcTableInfo class does not contain this info. While it's true that the GetSegmentSheet() method can be used for this, we would have to assume that segment zero is the proper segment to use.


Full Example: Replacing Formats

This example shows a complete code set for obtaining the drawing object, the format object, and their related info. It then procedes to call the ReplaceFormat() function, which removes the old format tables prior to replacing the format. The code replaces the format and tables very quickly.

// get current drawing and sheet number
var session = pfcGetProESession();
var drawing = session.CurrentModel;
var drwSheetNum = drawing.CurrentSheetNumber;

// get format name from html text field
var frmName = document.getElementById("frmName").value

// use format sheet 2 if drawing sheet is not sheet 1
var frmSheetNum = 1;
if (drwSheetNum > 1) { frmSheetNum = 2; }

// get the format
var frmDescr = pfcCreate("pfcModelDescriptor").CreateFromFileName(frmName);
var format = session.RetrieveModel(frmDescr);

// replace the format
ReplaceFormat(drawing, drwSheetNum, format, frmSheetNum);

// update the display (optional)
// session.CurrentWindow.Repaint();
// drawing.Regenerate();


function ReplaceFormat ( drawing, drwSheetNum, format, frmSheetNum ) {

var tables = null;

try {
// get sequence of tables in the drawing
tables = drawing.ListTables();
for (var i=0; i<tables.Count; i++) {
var table = tables.Item(i);
if (table.CheckIfIsFromFormat(drwSheetNum) == true) {
// table is from format, delete it
drawing.DeleteTable(table, false);
}
}
}
catch (e) {
// ignore, no tables in drawing
}

drawing.SetSheetFormat(drwSheetNum, format, frmSheetNum, null);

}
 
Another step to perform (marked as optional in the example) is to update the display using either Repaint() or Regenerate(). They both should accomplish the same goal, but one may work better in certain drawings than the other. You can be sure that if a large drawing is slow to repaint when performed in Pro/Engineer interactively, it will be just as slow via Pro/Web.Link.

Monday, June 15, 2009

Java: JOptionPane Examples Part 4 - Complex

Break out of the JOptionPane Limitations with JDialog

In the final article in my JOptionPane example series, I will show how to eliminate a JOptionPane limitation that you may encounter. The limitation is size. Sure, the dialog automatically sizes itself to fit your GUI elements. However, there is no way to allow a user to resize the standard JOptionPane dialog.

The way to eliminate this problem is to embed a JOptionPane object within a JDialog. It's very simple and requires very little change to existing code you may have.

Example 1: Resizable and Editable JTextArea with JScrollPane scrollbars

This example expands upon Example 2 (from Part 2). A JTextArea is used, but the dialog is not resizable. By embedding with a JDialog, this limitation can be overcome.

The definition of the JTextArea and Object array remains the same:

    JTextArea area = new JTextArea();
area.setText("line1\nline2\nline3\nline4\nline5\nline6");
area.setRows(5);
area.setColumns(10);
area.setEditable(true);
JScrollPane scrollpane = new JScrollPane(area);

Object[] array = {
new JLabel("Enter some text:"),
scrollpane,
};
 

Instead of using one of the static methods of JOptionPane, a JOptionPane object is created using its constructor. The Object array and the message type are passed as arguments to the constructor.

    JOptionPane pane = new JOptionPane(array, JOptionPane.PLAIN_MESSAGE);
 

Once the JOptionPane object has been created, the createDialog() method is called, which returns a JDialog object. null is passed to createDialog() to center the dialog on the screen, while the string argument will be the title of the dialog.

    JDialog dialog = pane.createDialog(null, "Result Data");
 

Before the setVisible() method is called, setResizable() is called to allow the dialog to be resized. When setVisible() is called, the dialog will exhibit the same general behavior as when using the static methods of JOptionPane.

    dialog.setResizable(true);
dialog.setVisible(true);
 

Once the user has closed the dialog, control returns to the program. As with the original version of the program, the JTextArea object still exists and its value can be obtained using getText().

    String newtext = area.getText();
System.out.println( "newtext: " + newtext );
 

With JDialog, many new options are available.

Wednesday, June 10, 2009

J-Link: Getting Started with Java and Pro/Engineer, Part 2

J-Link Java Code Requirements

J-Link requires that you have static methods with names matching the java_app_start and java_app_stop options from the registry file. Using the example registry file from Part 1, the required signatures for the "start" and "stop" methods would look like this:

    public static void startApp();
public static void stopApp();
 

As you'll see from the code that follows, these static methods don't do much but call other methods where all the important stuff happens. This approach seems to work well, but there are certainly other ways to approach J-Link.

Method Summary:

Here are the other methods used by the program, none of which are static:

Constructor JLinkHelloWorld()

The constructor sets up the needed utility items, such as the program name (based on the class name), a FileWriter object for the log file, and the platform specific newline character.

Method startupApplication()

The startupApplication() method gets the Pro/Engineer session object, defines a UI command, registers the UI command in the GUI (in the 'Tools' menu), and then announces to the user that the program is operational.

Method shutdownApplication()

The shutdownApplication() method closes out the log file's FileWriter object.

Method writeLog(String mesg)

A log file can be used for debugging and reporting purposes. A developer will use it for debugging an application, while an end user will be able to see what the program is doing internally.

Although using the log file is definitely optional, you'll find that there are all sorts of bizarre things in Pro/Engineer models that you will never have anticipated. With the use of proper logging, a developer increases the chances that these anomalies can be reported back by the end user.

Method closeLog()

As you might expect, this closes the log file.

Method DisplayMessage(String mesg)

DisplayMessage() writes an arbitrary message to the Pro/Engineer message area for the user to see. The example uses of DisplayMessage() that I have provided is not really the best approach for internationalization, but it serves the purpose of showing how to write message to the Pro/Engineer GUI. This method also writes the same output to the log file.

UI Command Registration and UI Listeners

A program is not going to be useful to an end user unless they can run the program. The user is not going to be able to run a J-Link program unless it registers itself with the Pro/Engineer GUI via a new menu or new menu option.

UI command registration is a multistep action requiring a couple of different pieces. The pieces required are a UICommandActionListener and a callback method, while the actions involve creating a command using UICreateCommand() and adding the command to the GUI using UIAddButton(). The text strings used as arguments to the UI methods are either found in the application's text message file (i.e. "JLHW Btn1 Label") or are known by Pro/Engineer (i.e. "Utilities" which represents the "Tools" menu).

The example program implements the listener by defining an inner class that extends the DefaultUICommandActionListener class. The inner class has a single method OnCommand() that is triggered when the user clicks on the command in the GUI. OnCommand() calls Btn1_callback() which is in the main class. Btn1_callback() then gets the current model object, if any, and displays the name in the message window using DisplayMessage().


The J-Link "Hello World" Java Code:

// JLinkHelloWorld.java
// Copyright 2009, MarcMettes@InversionConsulting.com

// imports required
import com.ptc.cipjava.*;
import com.ptc.pfc.pfcCommand.*;
import com.ptc.pfc.pfcGlobal.*;
import com.ptc.pfc.pfcModel.*;
import com.ptc.pfc.pfcSession.*;
import java.io.*;

public class JLinkHelloWorld {

static JLinkHelloWorld App = null;
String programName = null;
Session session = null;
FileWriter log = null;
String msgFile = "msg_jlinkhelloworld.txt";
String newline = null;

// constructor
//
public JLinkHelloWorld () {
programName = this.getClass().getName();
try {
log = new FileWriter(programName + ".log");
newline = System.getProperty("line.separator");
}
catch (Exception e) {
// couldn't create log file, ignore
}
}

// Display message in Pro/Engineer
//
public void DisplayMessage ( String mesg ) throws Exception {
stringseq seq = stringseq.create();
seq.set(0, mesg);
session.UIDisplayMessage(msgFile, "JLHW %s", seq);
seq.clear();
writeLog(mesg);
}

// Write text to log file
//
public void writeLog ( String mesg ) {
try {
if (log == null) { return; }
log.write(mesg + newline);
log.flush();
}
catch (Exception e) {
// ignore
}
}

// Close log file
//
public void closeLog () {
try {
if (log == null) { return; }
log.close();
}
catch (Exception e) {
// ignore
}
}

// Called by Pro/Engineer when starting the application
//
public static void startApp () {
try {
App = new JLinkHelloWorld();
App.startupApplication();
}
catch (Exception e) {
App.writeLog("Problem running startupApplication method" + e.toString());
return;
}
}

// Called by Pro/Engineer when stopping the application
//
public static void stopApp () {
try {
App.shutdownApplication();
}
catch (Exception e) {
App.writeLog("Problem running shutdownApplication method" + e.toString());
return;
}
}

// Perform some steps when shutting down the application
//
public void shutdownApplication () throws Exception {
writeLog("Application '" + programName + "' stopped");
closeLog();
}

// Perform some steps when starting the application
//
public void startupApplication () throws Exception {

try {
writeLog("Application '" + programName + "' started.");
session = pfcGlobal.GetProESession();
}
catch (jxthrowable x) {
writeLog("ERROR: Problem getting session object.");
return;
}

UICommand btn1_cmd = null;

try {
// Define a UI command
btn1_cmd = session.UICreateCommand(
"JLHW Btn1 Cmd", new JLHW_Btn1_CmdListener()
);
}
catch (jxthrowable x) {
writeLog("ERROR: Problem creating uicmd.");
return;
}

try {
// Add UI command to 'Tools' menu
session.UIAddButton(
btn1_cmd, "Utilities", null,
"JLHW Btn1 Label", "JLHW Btn1 Help",
"msg_jlinkhelloworld.txt"
);
}
catch (jxthrowable x) {
writeLog("ERROR: Problem creating menu: " + x.toString());
return;
}

DisplayMessage(programName + " application started.");

}

// Callback for the 'Tools' menu button
//
public void Btn1_callback ( ) throws Exception {

String mesg = null;
Model model = session.GetCurrentModel();

if (model == null) {
mesg = "Hello!";
}
else {
mesg = "Hello! The model is: " + model.GetFileName();
}

DisplayMessage(mesg);

}

// Inner class for UI Command Listener
//
public class JLHW_Btn1_CmdListener extends DefaultUICommandActionListener {

// Handler for button push
//
public void OnCommand () {
try {
Btn1_callback();
}
catch (Exception e) {
writeLog("Exception thrown by Btn1_callback method: " + e.toString());
}
}

}

}
 


As you can see, there are a number of non-trivial steps involved in creating J-Link applications. However, creating simple programs is really easy.

If you need a J-Link program written for you, please contact me at MarcMettes@InversionConsulting.com to discuss your requirements.